原文出處:http://www.yund.tech/zdetail.html?type=1&id=585ee331353551a44b29a9e9a09a1570
作者: jstarseven
一、前言
Redis 提供了5種數據類型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每種數據類型的特點對於redis的開發和運維非常重要。
二、疑問與解析
結構圖上顯示,String類型有三種實現方式:
- 使用整數值實現的字符串對象
- 使用 embstr 編碼的動態字符串實現的字符串對象
- 動態字符串實現的字符串對象
疑問:embstr 是什么意思,動態字符串又是什么意思?字符串對象到底什么結構?三種實現方式有什么區別呢?
不急,咱們一步一步的往下看:
1、Redis中定義的對象的結構體
/*
* Redis 對象
*/
typedef struct redisObject {
// 類型 4bits
unsigned type:4;
// 編碼方式 4bits
unsigned encoding:4;
// LRU 時間(相對於 server.lruclock) 24bits
unsigned lru:22;
// 引用計數 Redis里面的數據可以通過引用計數進行共享 32bits
int refcount;
// 指向對象的值 64-bit
void *ptr;
} robj;// 16bytes
注釋:type表示該對象的類型,即上面 [String,List,Hash,Set,Zset] 中的一個,但為了提高存儲效率與程序執行效率,每種對象的底層數據結構實現都可能不止一種,encoding 表示對象底層所使用的編碼。
2、Redis對象底層八種數據結構
REDIS_ENCODING_INT(long 類型的整數)
REDIS_ENCODING_EMBSTR embstr (編碼的簡單動態字符串)
REDIS_ENCODING_RAW (簡單動態字符串)
REDIS_ENCODING_HT (字典)
REDIS_ENCODING_LINKEDLIST (雙端鏈表)
REDIS_ENCODING_ZIPLIST (壓縮列表)
REDIS_ENCODING_INTSET (整數集合)
REDIS_ENCODING_SKIPLIST (跳躍表和字典)
3、embstr與動態字符串
embstr :是專門用於保存短字符串的一種優化編碼方式,跟正常的字符編碼相比,字符編碼會調用兩次內存分配函數來分別創建 redisObject 和 sdshdr 結構(動態字符串結構),而 embstr 編碼則通過調用一次內存分配函數來分配一塊連續的內存空間,空間中包含 redisObject 和 sdshdr(動態字符串)兩個結構,兩者在同一個內存塊中。從 Redis 3.0 版本開始,字符串引入了 embstr 編碼方式,長度小於 OBJ_ENCODING_EMBSTR_SIZE_LIMIT(39) 的字符串將以EMBSTR方式存儲。
注意: 在Redis 3.2 之后,就不是以 39 為分界線,而是以 44 為分界線,主要與 Redis 中內存分配使用的是 jemalloc 有關。( jemalloc 分配內存的時候是按照 8、16、32、64 作為 chunk 的單位進行分配的。為了保證采用這種編碼方式的字符串能被 jemalloc 分配在同一個 chunk 中,該字符串長度不能超過64,故字符串長度限制
OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 64 - sizeof('0')為1 - sizeof(robj) 為16 - sizeof(struct sdshdr)為8 = 39)
動態字符串 :Redis 自己構建的一種名為 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作為 Redis 的默認字符串表示。先簡單了解概念,后面看詳細解析
4、帶着疑問來細品下面一段話
字符串的編碼可以是 int,raw 或者 embstr。如果一個字符串內容可轉為 long,那么該字符串會被轉化為 long 類型,對象 ptr 指向該 long,並且對象類型也用 int 類型表示。普通的字符串有兩種 embstr 和 raw。如果字符串對象的長度小於 39 字節,就用 embstr,否則用 raw。
也就是說,Redis 會根據當前值的類型和長度決定使用內部編碼實現:恍然大悟
int:8個字節的長整型
embstr:小於等於39個字節的字符串
raw:大於39個字節的字符串
5、實踐驗證
命令:object encoding key ,獲取數據底層的數據結構
1)整數類型示例如下:
2)短字符串示例如下:
3)長字符串示例如下:
疑問:至此,我們知道了embstr、字符串對象, 但是動態字符串的結構還是沒說清楚啊,你是不是在逗我?
靚仔疑問,再一次出現,別急,繼續往下看

三、動態字符串
眾所周知,Redis 是用 C 語言寫的,但是對於 Redis 的字符串,卻不是 C 語言中的字符串(即以空字符 ’\0’ 結尾的字符數組),它是自己構建了一種名為 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作為 Redis 的默認字符串表示。
1、動態字符串結構分析
SDS 定義:
struct sdshdr{
//記錄buf數組中已使用字節的數量
//等於 SDS 保存字符串的長度 4byte
int len;
//記錄 buf 數組中未使用字節的數量 4byte
int free;
//字節數組,用於保存字符串 字節\0結尾的字符串占用了1byte
char buf[];
}
用 SDS 保存字符串 “Redis” 具體結構如下圖

對於 SDS 數據類型的定義:
- len 保存了SDS保存字符串的長度
- buf[] 數組用來保存字符串的每個元素
- free 記錄了 buf 數組中未使用的字節數量
上面的定義相對於 C 語言對於字符串的定義,多出了 len 屬性以及 free 屬性。為什么不直接使用 C 語言字符串實現,而是要使用 SDS 呢?有什么特別的優勢呢?
2、SDS結構與C語言字符串結構比較分析

1)獲取字符串長度復雜度
sdshdr 中由於 len 屬性的存在,獲取 SDS 字符串的長度只需要讀取 len 屬性,時間復雜度為 O(1),而對於 C 語言來說,獲取字符串的長度通常是遍歷字符串計數來實現的,時間復雜度為 O(n)。
2)API安全性與緩沖區溢出
緩沖區溢出(buffer overflow):是這樣的一種異常,當程序將數據寫入緩沖區時,會超過緩沖區的邊界,並覆蓋相鄰的內存位置。在 C 語言中使用 strcat 函數來進行兩個字符串的拼接,一旦沒有分配足夠長度的內存空間,就會造成緩沖區溢出,如

s1 = 'Redis',s2 = 'MongoDB',當執行strcat(s1, " Cluster")時,未給 s1 分配足夠內存空間,s1 的數據將溢出到 s2 所在的內存空間,導致 s2 保存的內容被意外地修改。

由於 SDS 記錄了自身長度,同時在修改時,API 會按照如下步驟進行:
(1)先檢查SDS的空間是否滿足修改所需的要求;
(2)如果不滿足要求的話,API 會自動將 SDS 的空間擴展至執行修改所需的大小(realloc);
(3)然后才執行實際的修改操作;
所以SDS不會造成緩沖區溢出情況
3)字符串的內存重分配次數
C 語言由於不記錄字符串的長度,所以如果要修改字符串,必須要重新分配內存。
SDS 實現了空間預分配和惰性釋放兩種策略:
(1)空間預分配:當 SDS 的 API 對一個 SDS 進行修改,並且需要對 SDS 進行空間擴展的時候,程序不僅會為 SDS 分配修改所必須的空間,還會為 SDS 分配額外的未使用空間,這樣可以減少連續執行字符串增長操作所需的內存重分配次數。
(2)惰性釋放:當 SDS 的 API 需要對 SDS 保存的字符串進行縮短時,程序並不立即使用內存重分配來回收縮短后多出來的字節,而是使用 free 屬性將這些字節的數量記錄起來,並等待將來使用,如

sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X' 和 'Y'
結果

4)二進制數據安全
二進制安全(binary-safe):指能處理任意的二進制數據,包括非 ASCII 和 null 字節。
C 字符串以空字符 '\0',作為字符串結束的標識,而對於一些二進制文件(如圖片等),內容可能包括空字符串'\0',導致程序讀入的空字符會被誤認為是字符串的結尾,因此C字符串無法正確存取二進制數據;
SDS 的 API 都是以處理二進制的方式來處理 buf 里面的元素,並且 SDS 不是以空字符串'\0'來判斷是否結束,而是以 len 屬性表示的長度來判斷字符串是否結束,
因此 Redis 不僅可以保存文本數據,還可以保存任意格式的二進制數據。
5)C字符串函數兼容
SDS 的buf數組會以'\0'結尾,這樣可以重用 C 語言庫<string.h> 中的一部分函數,避免了不必要的代碼重復。
四、要點總結
String 類型對象三種實現方式,int,embstr,raw
字符串內容可轉為 long,采用 int 類型,否則長度<39(3.2版本前39,3.2版本后分界線44) 用 embstr,其他用 raw
SDS 是Redis自己構建的一種簡單動態字符串的抽象類型,並將 SDS 作為 Redis 的默認字符串表示
SDS 與 C 語言字符串結構相比,具有五大優勢