相信大家對 redis 的數據結構都比較熟悉:
為了將性能優化到極致,redis 作者為每種數據結構提供了不同的實現方式,以適應特定應用場景。
以最常用的 string 為例,其底層實現就可以分為 3 種: int , embstr , raw
127.0.0.1:6379> SET counter 1OK127.0.0.1:6379> OBJECT ENCODING counter"int"127.0.0.1:6379> SET name "Tom"OK127.0.0.1:6379> OBJECT ENCODING name"embstr"127.0.0.1:6379> SETBIT bits 1 1(integer) 0127.0.0.1:6379> OBJECT ENCODING bits"raw"
這些特定的底層實現在 redis 中被稱為 編碼 encoding ,下面逐一介紹這些編碼實現。
redis 中所有的 key 都是字符串,這些字符串是通過一個名為 簡單動態字符串 SDS 的數據結構實現的。
typedef char *sds; // SDS 字符串指針,指向 sdshdr.buf struct sdshdr? { // SDS header,[?] 可以為 8, 16, 32, 64 uint?_t len; // 已用空間,字符串的實際長度 uint?_t alloc; // 已分配空間,不包含'\0' unsigned char flags; // 類型標記,指明了 len 與 alloc 的實際類型,可以通過 sds[-1] 獲取 char buf[]; // 字符數組,保存以'\0'結尾的字符串,與傳統 C 語言中的字符串的表達方式保持一致 };
內存布局如下:
+-------+---------+-----------+-------+| len | alloc | flags | buf |+-------+---------+-----------+-------+ ^--sds[-1] ^--sds
相較於傳統的 C 字符串,其優點如下:
O(1)
redis 中 list 的底層實現之一是雙向鍊表,該結構支持順序訪問,並提供了高效的元素增刪功能。
typedef struct listNode { struct listNode *prev; // 前置節點 struct listNode *next; // 後置節點 void *value; // 節點值 } listNode; typedef struct list { listNode *head; // 頭節點 listNode *tail; // 尾節點 unsigned long len; // 列表長度 void *(*dup) (void *ptr); // 節點值複製函數 void (*free) (void *ptr); // 節點值釋放函數 int (*match) (void *ptr); // 節點值比較函數 } list;
這裡使用了函數指針來實現動態綁定,根據 value 類型,指定不同 dup , free , match 的函數,實現多態。
該數據結構有以下特徵:
O(1)O(1)
redis 中使用 dict 來保存鍵值對,其底層實現之一是哈希表。
typedef struct dictEntry { void* key; // 鍵 union { // 值,可以為指針、有符號長整,無符號長整,雙精度浮點 void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; typedef struct dictht { dictEntry **table; // 哈希表數組,數組中的每個元素是一個單向鍊表 unsigned long size; // 哈希表數組大小 unsigned long sizemask; // 哈希掩碼,用於計算索引 unsigned long used; // 已有節點數量 } dictht; typedef struct dictType { unsigned int (*hashFunction) (const void *key); // 哈希函數,用於計算哈希值 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 鍵比較函數 void *(*keyDup)(void *privdata, const void *key); // 鍵複製函數 void *(*valDup)(void *privdata, const void *obj); // 值複製函數 void *(*keyDestructor)(void *privdata, const void *key); // 鍵銷毀函數 void *(*valDestructor)(void *privdata, const void *obj); // 值銷毀函數 } dictType; typedef struct dict { dictType *type; // 類型函數,用於實現多態 void *privdata; // 私有數據,用於實現多態 dictht ht[2]; // 哈希表,字典使用 ht[0] 作為哈希表,ht[1] 用於進行 rehash int rehashidx; // rehash索引,當沒有執行 rehash 時,其值為 -1 } dict;
該數據結構有以下特徵:
跳表是一種 有序 數據結構,並且通過維持多層級指針來達到快速訪問的目的,是典型的空間換時間策略。
其查找效率與平衡樹相近,但是維護成本更低,且實現簡單。
typedef struct zskiplistNode { sds ele; // 成員對象 double score; // 分值 struct zskiplistNode *backward; // 後退指針 struct zskiplistLevel { struct zskiplistNode *forward; // 前進指針 unsigned long span; // 跨度,當前節點和前進節點之間的距離 } level[]; } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail;// 頭尾指針 unsigned long length; // 長度 int level; // 最大層級 } zskiplist;
該數據結構有以下特徵:
有序整型集合,具有緊湊的存儲空間,添加操作的時間複雜度為 O(N) 。
typedef struct intset { uint32_t encoding; // 編碼方式,指示元素的實際類型 uint32_t length; // 元素數量 int8_t contents[]; // 元素數組,元素實際類型可能為 int16_t,int32_t,int64_t, } intset;
該數據結構有以下特徵:
壓縮列表是為了節約內存而開發的,是存儲在連續內存塊上的順序數據結構。
一個壓縮列表可以包含任意多的 entry 節點,每個節點包含一個字節數組或整數。
redis 中並沒有顯式定義 ziplist 的數據結構,僅僅提供了一個描述結構 zlentry 用於操作數據。
typedef struct zlentry { unsigned int prevrawlensize;// 用於記錄前一個 entry 長度的字節數 unsigned int prevrawlen; // 前一個 entry 的長度 unsigned int lensize // 用於記錄當前 entry 類型/長度的字節數 unsigned int len; // 實際用於存儲數據的字節數 unsigned int headersize; // prevrawlensize + lensize unsigned char encoding; // 用於指示 entry 數據的實際編碼類型 unsigned char *p; // 指向 entry 的開頭 } zlentry;
其實際的內存布局如下:
+----------+---------+---------+--------+-----+--------+--------+| zlbytes | zltail | zllen | entry1 | ... | entryN | zlend |+----------+---------+---------+--------+-----+--------+--------+<--------------------------- zlbytes ---------------------------> ^--zltail <------- zllen ------->
entry 的內存布局如下:
+-------------------+----------+---------+| prev_entry_length | encoding | content |+-------------------+----------+---------+
該數據結構具有以下特徵:
在較早版本的 redis 中,list 有兩種底層實現:
兩者各有優缺點:
為了結合兩者的優點,在 redis 3.2 之後,list 的底層實現變為快速列表 quicklist。
快速列表是 linkedlist 與 ziplist 的結合: quicklist 包含多個內存不連續的節點,但每個節點本身就是一個 ziplist。
typedef struct quicklistNode { struct quicklistNode *prev; // 上一個 ziplist struct quicklistNode *next; // 下一個 ziplist unsigned char *zl; // 數據指針,指向 ziplist 結構,或者 quicklistLZF 結構 unsigned int sz; // ziplist 佔用內存長度(未壓縮) unsigned int count : 16; // ziplist 記錄數量 unsigned int encoding : 2; // 編碼方式,1 表示 ziplist ,2 表示 quicklistLZF unsigned int container : 2; // unsigned int recompress : 1; // 臨時解壓,1 表示該節點臨時解壓用於訪問 unsigned int attempted_compress : 1; // 測試欄位 unsigned int extra : 10; // 預留空間 } quicklistNode; typedef struct quicklistLZF { unsigned int sz; // 壓縮數據長度 char compressed[]; // 壓縮數據 } quicklistLZF; typedef struct quicklist { quicklistNode *head; // 列表頭部 quicklistNode *tail; // 列表尾部 unsigned long count; // 記錄總數 unsigned long len; // ziplist 數量 int fill : 16; // ziplist 長度限制,每個 ziplist 節點的長度(記錄數量/內存佔用)不能超過這個值 unsigned int compress : 16; // 壓縮深度,表示 quicklist 兩端不壓縮的 ziplist 節點的個數,為 0 表示所有 ziplist 節點都不壓縮 } quicklist;
該數據結構有以下特徵:
為了實現動態編碼技術,redis 構建了一個對象系統。
redis 可以在執行命令前,根據對象類型判斷當前命令是否能夠執行。
此外,該系統通過引用計數實現內存共享,並記錄來對象訪問時間,為優化內存回收策略提供了依據。
typedef struct redisObject { unsigned type:4; // 類型,當前對象的邏輯類型,例如:set unsigned encoding:4; // 編碼,底層實現的數據結構,例如:intset / ziplist unsigned lru:24; /* LRU 時間 (相對與全局 lru_clock 的時間) 或 * LFU 數據 (8bits 記錄訪問頻率,16 bits 記錄訪問時間). */ int refcount; // 引用計數 void *ptr; // 數據指針,指向具體的數據結構 } robj;
該數據結構有以下特徵:
string 的編碼類型可能為:
127.0.0.1:6379> SET str "1234567890 1234567890 1234567890 1234567890"OK127.0.0.1:6379> STRLEN str(integer) 43127.0.0.1:6379> OBJECT ENCODING str"embstr"127.0.0.1:6379> APPEND str _(integer) 44127.0.0.1:6379> OBJECT ENCODING str"raw"
使用 embstr 編碼是為了減少短字符串的內存分配次數,參考 redis 作者原話:
REDIS_ENCODING_EMBSTR_SIZE_LIMIT set to 39.
對比兩者內存布局可以發現:
embstrraw
<------------------------------------------ Jemalloc arena (64 bytes) ---------------------------------------------->+-------------------------------------------------------------------------------+---------------------+--------------+| redisObject (16 bytes) | sdshdr8 (3 bytes) | 45 bytes |+--------------------+---------------------------------+-------+----------+-----+-----+-------+-------+---------+----+| type(REDIS_STRING) | encoding(REDIS_ENCODING_EMBSTR) | lru | refcount | ptr | len | alloc | flags | buf | \0 |+--------------------+---------------------------------+-------+----------+-----+-----+-------+-------+---------+----++--------------------+| redisObject |+--------------------+| type || REDIS_STRING |+--------------------+| encoding || REDIS_ENCODING_RAW |+--------------------+ +---------+| ptr | ---> | sdshdr? |+--------------------+ +---------+ | len | +---------+ | alloc | +---------+ | flags | +---------++---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | buf || T | h | e | r | e | | i | s | | n | o | | c | e | r | t | a |...| +---------++---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
list 默認的編碼類型為 OBJ_ENCODING_QUICKLIST quicklist
hash 的編碼類型有 OBJ_ENCODING_ZIPLIST ziplist 與 OBJ_ENCODING_HT hashtable,具體使用哪種編碼受下面兩個選項控制:
key 長度超過 64 的情況:
127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'(integer) 0127.0.0.1:6379> OBJECT ENCODING table"ziplist"127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'(integer) 0127.0.0.1:6379> OBJECT ENCODING table"hashtable"127.0.0.1:6379> DEL table(integer) 1127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'(integer) 1127.0.0.1:6379> OBJECT ENCODING table"ziplist"127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'(integer) 1127.0.0.1:6379> OBJECT ENCODING table"hashtable"
value 長度超過 64 的情況:
127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'(integer) 0127.0.0.1:6379> OBJECT ENCODING table"ziplist"127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'(integer) 0127.0.0.1:6379> OBJECT ENCODING table"hashtable"127.0.0.1:6379> DEL table(integer) 1127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'(integer) 1127.0.0.1:6379> OBJECT ENCODING table"ziplist"127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x'(integer) 1127.0.0.1:6379> OBJECT ENCODING table"hashtable"
元素數量度超過 512 的情況:
127.0.0.1:6379> EVAL "for i=1,512 do redis.call('HSET', KEYS[1], i, i) end" 1 numbers(nil)127.0.0.1:6379> HLEN numbers(integer) 512127.0.0.1:6379> OBJECT ENCODING numbers"ziplist"127.0.0.1:6379> DEL numbers(integer) 1127.0.0.1:6379> EVAL "for i=1,513 do redis.call('HSET', KEYS[1], i, i) end" 1 numbers(nil)127.0.0.1:6379> HLEN numbers(integer) 513127.0.0.1:6379> OBJECT ENCODING numbers"hashtable"
set 的編碼類型有 OBJ_ENCODING_INTSET intset 與 OBJ_ENCODING_HT hashtable ,具體使用哪種編碼受下面兩個選項控制:
包含非整數元素的情況:
127.0.0.1:6379> SADD set 1 2(integer) 2127.0.0.1:6379> OBJECT ENCODING set"intset"127.0.0.1:6379> SADD set "ABC"(integer) 1127.0.0.1:6379> OBJECT ENCODING set"hashtable"
元素數量度超過 512 的情況:
127.0.0.1:6379> EVAL "for i=1,512 do redis.call('SADD', KEYS[1], i, i) end" 1 numbers(nil)127.0.0.1:6379> SCARD numbers(integer) 512127.0.0.1:6379> OBJECT ENCODING numbers"intset"127.0.0.1:6379> DEL numbers(integer) 1127.0.0.1:6379> EVAL "for i=1,513 do redis.call('SADD', KEYS[1], i, i) end" 1 numbers(nil)127.0.0.1:6379> SCARD numbers(integer) 513127.0.0.1:6379> OBJECT ENCODING numbers"hashtable"
set 的編碼類型有 OBJ_ENCODING_ZIPLIST ziplist 與 OBJ_ENCODING_SKIPLIST skiplist 。
使用 ziplist 編碼時,每個集合元素使用兩個相鄰的 entry 節點保存,第一個節點保存成員值 member,第二節點保存元素的分值 score,並且 entry 按照 score 從小到大進行排序:
+----------------------+| redisObject |+----------------------+| type || REDIS_ZSET |+----------------------+| encoding || OBJ_ENCODING_ZIPLIST |+----------------------+ +----------+----------+---------+--------------------+-------------------+-----+-----------------------+--------------------+-------+| ptr | ---> | zlbytes | zltail | zllen | entry 1 (member 1) | entry 2 (score 1) | ... | entry 2N-1 (member N) | entry 2N (score N) | zlend |+----------------------+ +----------+----------+---------+--------------------+-------------------+-----+-----------------------+--------------------+-------+ >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> score increase >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
使用 skiplist 實現時,使用會使用一個名為 zset 的數據結構:
typedef struct zset { dict *dict; // 維護 member -> score 的映射,查找給的成員的分值 zskiplist *zsl; // 按 score 大小保存了所有集合元素,支持範圍操作 } zset; // dict 與 zsl 會共享成員與分值
+----------------------+ +--------+ +------------+ +---------+| redisObject | +-->| dictht | | StringObj | -> | long |+----------------------+ +-------+ | +--------+ +------------+ +---------+| type | +-->| dict | | | table | --> | StringObj | -> | long || REDIS_ZSET | | +-------+ | +--------+ +------------+ +---------++----------------------+ | | ht[0] | --+ | StringObj | -> | long || encoding | +--------+ | +-------+ +-----+ +------------+ +---------+| OBJ_ENCODING_ZIPLIST | | zset | | | L32 | -> NULL+----------------------+ +--------+ | +-----+| ptr | ---> | dict | --+ | ... |+----------------------+ +--------+ +--------+ +-----+ +-----------+ +-----------+ | zsl | ---> | header | --> | L4 | -> | L4 | ------------------> | L4 | -> NULL +--------+ +--------+ +-----+ +-----------+ +-----------+ | tail | | L3 | -> | L3 | ------------------> | L3 | -> NULL +--------+ +-----+ +-----------+ +-----------+ +-----------+ | level | | L2 | -> | L2 | -> | L2 | -> | L2 | -> NULL +--------+ +-----+ +-----------+ +-----------+ +-----------+ | length | | L1 | -> | L1 | -> | L1 | -> | L1 | -> NULL +--------+ +-----+ +-----------+ +-----------+ +-----------+ NULL <- | BW | <- | BW | <- | BW | +-----------+ +-----------+ +-----------+ | StringObj | | StringObj | | StringObj | +-----------+ +-----------+ +-----------+ | long | | long | | long | +-----------+ +-----------+ +-----------+
zset 具體使用哪種編碼受下面兩個選項控制:
每個資料庫都是一個 redisDb 結構體:
typedef struct redisDb { dict *dict; /* 據庫的鍵空間 keyspace */ dict *expires; /* 設置了過期時間的 key 集合 */ dict *blocking_keys; /* 客戶端阻塞等待的 key 集合 (BLPOP)*/ dict *ready_keys; /* 已就緒的阻塞 key 集合 (PUSH) */ dict *watched_keys; /* 在事務中監控受監控的 key 集合 */ int id; /* 資料庫 ID */ long long avg_ttl; /* 平均 TTL, just for stats */ unsigned long expires_cursor; /* 過期檢測指針 */ list *defrag_later; /* 內存碎片回收列表 */ } redisDb;
redis 所有資料庫都保存著 redisServer.db 數組中,redisServer.dbnum 保存了資料庫的數量,簡化後的內存布局大致如下:
+-------------+| redisServer |+-------------+ +------------+------+-------------+| db | -> | redisDb[0] | .... | redisDb[15] |+-------------+ +------------+------+-------------+| dbnum | || 16 | |+-------------+ | +---------+ +------------+ +->| redisDb | +-> | ListObject | +---------+ +------------+ | +------------+ | dict | -> | StringObj | --+ +---------+ +------------+ +------------+ | expires | | StringObj | ----> | HashObject | +---------+ +------------+ +------------+ | | StringObj | --+ | +------------+ | +------------+ | +-> | StringObj | | +------------+ | | +------------+ +-------------+ +----> | StringObj | -> | long | +------------+ +-------------+ | StringObj | -> | long | +------------+ +-------------+
至此,redis 的幾種編碼方式都介紹完畢,後續將對 redis 的一些其他細節進行分享,感謝觀看。
來源:https://www.tuicool.com/articles/A3aU3mj