相信你們對 redis 的數據結構都比較熟悉:redis
爲了將性能優化到極致,redis 做者爲每種數據結構提供了不一樣的實現方式,以適應特定應用場景。
以最經常使用的 string 爲例,其底層實現就能夠分爲 3 種:int
, embstr
, raw
算法
127.0.0.1:6379> SET counter 1 OK 127.0.0.1:6379> OBJECT ENCODING counter "int" 127.0.0.1:6379> SET name "Tom" OK 127.0.0.1:6379> OBJECT ENCODING name "embstr" 127.0.0.1:6379> SETBIT bits 1 1 (integer) 0 127.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
的函數,實現多態。數據結構
該數據結構有如下特徵:ide
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;
該數據結構有如下特徵:
哈希算法:使用 murmurhash2 做爲哈希函數,時間複雜度爲O(1)
衝突解決:使用鏈地址法解決衝突,新增元素會被放到表頭,時間複雜度爲O(1)
從新散列:每次 rehash 操做都會分紅 3 步完成
步驟1:爲dict.ht[1]
分配空間,其大小爲 2 的 n 次方冪
步驟2:將dict.ht[0]
中的全部鍵值對 rehash 到dict.ht[1]
上
步驟3:釋放dict.ht[0]
的空間,用dict.ht[1]
替換 dict.ht[0]
分攤開銷
爲了減小停頓,步驟2 會分爲屢次漸進完成,將 rehash 鍵值對所需的計算工做,平均分攤到每一個字典的增長、刪除、查找、更新操做,期間會使用dict.rehashidx
記錄dict.ht[0]
中已經完成 rehash 操做的dictht.table
索引:
dict.rehashidx
計數器會加 1dict.rehashidx
會被設置爲 -1觸發條件
計算當前負載因子:loader_factor = ht[0].used / ht[0].size
收縮: 當 loader_factor < 0.1 時,執行 rehash 回收空閒空間
擴展:
大多操做系統都採用了 寫時複製copy-on-write
技術來優化子進程的效率:
父子進程共享同一份數據,直到數據被修改時,才實際拷貝內存空間給子進程,保證數據隔離
在執行 BGSAVE 或 BGREWRITEAOF 命令時,redis 會建立子進程,此時服務器會經過增長 loader_factor 的閾值,避免在子進程存在期間執行沒必要要的內存寫操做,節約內存
跳錶是一種有序數據結構,而且經過維持多層級指針來達到快速訪問的目的,是典型的空間換時間策略。
其查找效率與平衡樹相近,可是維護成本更低,且實現簡單。
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(logN)
,最壞查找時間爲O(N)
,而且支持範圍查找有序整型集合,具備緊湊的存儲空間,添加操做的時間複雜度爲O(N)
。
typedef struct intset { uint32_t encoding; // 編碼方式,指示元素的實際類型 uint32_t length; // 元素數量 int8_t contents[]; // 元素數組,元素實際類型可能爲 int16_t,int32_t,int64_t, } intset;
該數據結構有如下特徵:
有序:元素數組中的元素按照從小到大排列,使用二分查找時間複雜度爲O(logN)
升級:當有新元素加入集合,且新元素比全部現有元素類型都長時,集合須要進行升級:
步驟1:根據新元素的類型,擴展元素數組空間
步驟2:將現有元素都轉換爲新類型
步驟3:將新元素添加到數組中
壓縮列表是爲了節約內存而開發的,是存儲在連續內存塊上的順序數據結構。
一個壓縮列表能夠包含任意多的 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 | +-------------------+----------+---------+
該數據結構具備如下特徵:
O(N)
O(N2)
在較早版本的 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 的編碼類型可能爲:
int
:long 類型整數raw
:sds 字符串embstr
:嵌入式字符串(編碼後長度小於 44 字節的字符串)127.0.0.1:6379> SET str "1234567890 1234567890 1234567890 1234567890" OK 127.0.0.1:6379> STRLEN str (integer) 43 127.0.0.1:6379> OBJECT ENCODING str "embstr" 127.0.0.1:6379> APPEND str _ (integer) 44 127.0.0.1:6379> OBJECT ENCODING str "raw"
使用 embstr
編碼是爲了減小短字符串的內存分配次數,參考 redis 做者原話:
對比二者內存佈局能夠發現:
embstr
是一個完整連續的內存塊,只須要 1 次內存分配raw
的內存是不連續的,須要申請 2 次內存<------------------------------------------ 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) 0 127.0.0.1:6379> OBJECT ENCODING table "ziplist" 127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (integer) 0 127.0.0.1:6379> OBJECT ENCODING table "hashtable" 127.0.0.1:6379> DEL table (integer) 1 127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x' (integer) 1 127.0.0.1:6379> OBJECT ENCODING table "ziplist" 127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x' (integer) 1 127.0.0.1:6379> OBJECT ENCODING table "hashtable"
value 長度超過 64 的狀況:
127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (integer) 0 127.0.0.1:6379> OBJECT ENCODING table "ziplist" 127.0.0.1:6379> HSET table x 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' (integer) 0 127.0.0.1:6379> OBJECT ENCODING table "hashtable" 127.0.0.1:6379> DEL table (integer) 1 127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x' (integer) 1 127.0.0.1:6379> OBJECT ENCODING table "ziplist" 127.0.0.1:6379> HSET table xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 'x' (integer) 1 127.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) 512 127.0.0.1:6379> OBJECT ENCODING numbers "ziplist" 127.0.0.1:6379> DEL numbers (integer) 1 127.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) 513 127.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) 2 127.0.0.1:6379> OBJECT ENCODING set "intset" 127.0.0.1:6379> SADD set "ABC" (integer) 1 127.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) 512 127.0.0.1:6379> OBJECT ENCODING numbers "intset" 127.0.0.1:6379> DEL numbers (integer) 1 127.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) 513 127.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 的一些其餘細節進行分享,感謝觀看。