Redis 設計與實現[1] -- 資料結構與物件
1 簡單動態字串
Redis 沒有直接使用 C 語言傳統的字串表示,而是自己構建了一種簡單動態字串(SDS),使用 SDS 作為 REdis 的預設字串表示。
1.1 SDS 定義
struct sdshdr {
// 記錄 buf 陣列中已經使用位元組的數量,等於 SDS 所儲存字串的長度
int len;
// 記錄 buf 陣列中未使用位元組的數量
int free;
// 位元組陣列,用於儲存字串
char buf[];
};
1.2 SDS 與 C 字串的區別
- 常數複雜度獲取字串的長度
- 杜絕緩衝區溢位
- 減少修改字串時帶來的記憶體重分配次數
- 二進位制安全
- 相容部分 C 字串函式
2 連結串列
連結串列在 Redis 中的應用非常廣泛,比如列表鍵的底層實現之一就是連結串列。當一個列表鍵包含了數量比較多的元素,又或者列表中包含的元素都是比較長的字串時,Redis 就會使用連結串列作為列表鍵的底層實現。
2.1 Redis 連結串列結構
typedef struct list { // 表頭節點 listNode *head; // 表尾節點 listNode *tail; // 連結串列所包含的節點數量 unsigned long len; // 節點值複製函式 void *(*dup)(void *ptr); // 節點值釋放函式 void (*free)(void *ptr); // 節點值對比函式 int (*match)(void *ptr, void *key); } list;
由一個 list 結構和三個 listNode 結構組成的連結串列如下圖所示:
2.2 Redis 連結串列特性
- 雙端
- 無環
- 帶表頭指標和表尾指標
- 帶連結串列長度計數器
- 多型
3 字典
字典,是一種用於儲存鍵值對的抽象資料結構,在字典中一個鍵可以和一個值進行關聯,這些關聯的鍵和值就稱為鍵值對。Redis 的字典使用雜湊表作為底層實現,一個雜湊表裡面可以有多個雜湊表節點,而每個雜湊表節點就儲存了字典中的一個鍵值對。
3.1 字典實現
Redis 中的字典結構為:
typedef struct dict { // 型別特定函式 dictType *type; // 私有資料 void *privdata; // 雜湊表 dictht ht[2]; // rehash 索引,rehash 不在進行時,值為 -1 int trehashidx; }dict;
ht 屬性是一個包含兩個項的陣列,陣列中的每個項都是一個 dictht 雜湊表,一般情況下,字典只使用 ht[0] 雜湊表,ht[1] 雜湊表只會在對 ht[0] 雜湊表進行 rehash 時使用。
下圖展示一個普通狀態下的字典,其中紅框中的是雜湊表:
3.2 雜湊
- 當字典被用作資料庫的底層實現,或者雜湊鍵的底層實現,Redis 使用 MurmurHash2 演算法來計算鍵的雜湊值。
- 雜湊表使用鏈地址法來解決鍵衝突,被分配到同一個索引上的多個鍵值對會連線成一個單向連結串列
- 在對雜湊表進行擴充套件或者收縮操作時,程式需要將現有的雜湊表包含的所有鍵值對 rehash 到新的雜湊表裡面,並且這個 rehash 過程並不是一次性地完成的,而是漸進式地完成的。
4 跳躍表
跳躍表是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。
跳躍表支援平均 O(logN),最壞 O(N) 的複雜度的節點查詢。Redis 使用跳躍表作為有序集合鍵的底層實現之一。
跳躍表是由 zskiplistNode 和 zskiplist 兩個結構定義,其中 zskiplistNode(灰框) 結構用於表示跳躍表節點,zskiplist(紅框) 結構用於儲存跳躍表節點的相關資訊,如下表所示:
-
zskiplist
header:指向跳躍表的表頭節點
tail:指向跳躍表的表尾節點
level:記錄目前跳躍表內,層數最大的那個節點的層數
length:記錄跳躍表的長度
-
zskiplistNode
level:層
backward:後退指標
score:分值
obj:成員物件
5 整數集合
整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis 就會使用整數集合作為集合鍵的底層實現,它可以儲存型別為 int16_t、int32_t、int64_t 的整數值。
5.1 整數集合結構
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 儲存元素的陣列
int8_t contents[];
}inset;
5.2 升級
當我們要將一個新的元素新增到整數集合裡面,並且新元素的型別比整數集合現有的所有元素的型別都要長,整數集合需要先進行升級,然後才能將新元素新增到整數集合裡面。
升級方式:
- 根據新元素的型別,擴充套件整數集合底層陣列的空間大小,併為新元素分配空間
- 將底層陣列現在的所有元素都轉換成與新元素相同的型別,並將型別轉換後的元素放置到正確的位置上,並且在放置元素過程中,需要維持底層陣列的有序性質不變
- 將新元素新增到底層數組裡面
升級優點:
- 提升整數集合的靈活性
- 節約記憶體
6 壓縮列表
壓縮列表是列表鍵和雜湊鍵的底層實現之一,當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數值,要麼就是長度比較短的字串,那麼 Redis 就會使用壓縮列表來做列表建的底層實現。此外,當一個雜湊鍵只包含少量鍵值對,並且每個鍵值對的鍵和值要麼就是小整數值,要麼就是長度比較短的字串,那麼 Redis 就會使用壓縮列表來做雜湊鍵的底層實現。
7 物件
Redis 物件型別:字串物件REDIS_STRING
、列表物件REDIS_LIST
、雜湊物件REDIS_HASH
、集合物件REDIS_SET
、有序集合物件REDIS_ZSET
7.1 物件型別與編碼
Redis 中的每個物件都是一個 redisObject 結構表示:
type struct redisObject {
// 型別
unsigned type:4;
// 編碼
unsigned encoding:4;
// 指向底層實現資料結構的指標
void *ptr;
//...
} robj;
通過 encoding 屬性來設定物件所使用的編碼,可以使得 Redis 根據不同的使用場景為一個物件設定不同的編碼,從而優化物件在某一場景下的效率,舉個例子,在列表物件包含的元素較少的時候,Redis 使用壓縮列表作為列表物件的底層實現:
- 因為壓縮列表比雙端列表更節約記憶體,並且在元素數量較少的時候,在記憶體中以連續塊方式儲存的壓縮列表比起雙端連結串列可以更快被載入到快取中
- 隨著列表物件包含的元素越來越多,使用壓縮列表來儲存元素的優勢逐漸消失,物件就會將底層實現從壓縮列表轉向功能更強、也更適合儲存大量元素的雙端連結串列。
7.2 字串物件
字串物件的編碼可以是 int,raw,embstr
- 如果一個字串物件儲存的是整數值,並且這個整數值可以用 long 型別來表示,那麼編碼設定為 int
- 如果字串物件物件儲存是一個字串值,並且這個字串值的長度大於 32 位元組,那麼字串物件將使用 SDS 來儲存這個字串值,並將物件的編碼設定為 raw
- 如果儲存的字串值的長度小於等於 32 位元組,那麼字串物件將使用 embstr 編碼的方式來儲存這個字串值(分配記憶體和釋放記憶體均從 raw 的兩次降為一次)
- long double 浮點型也是作為字串值來儲存的
7.3 列表物件
列表物件的編碼可以是 ziplist 或者 linkedlist
- ziplist 每個壓縮列表節點(entry)儲存一個列表元素
- linkedlist 每個雙端連結串列節點(node)都儲存一個字串物件,每個字串物件都儲存一個列表元素
列表物件同時滿足以下兩個條件的時候列表物件使用 ziplist 編碼:
- 列表物件儲存的所有字串元素的長度都小於 64 位元組
- 列表物件儲存的元素數量小於 512 個
7.4 雜湊物件
雜湊物件的編碼可以是 ziplist 或者 hashtable
- ziplist 編碼的雜湊物件使用壓縮表作為底層實現,儲存同一鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後;先新增的在表頭方向,後新增的在表尾方向
- hashtable 使用字典作為底層實現,每個鍵值對都使用一個字典鍵值對來儲存:字典的每個鍵都是一個字串物件,物件中儲存了鍵值對的鍵;字典中每個值都是字串物件,物件中儲存了鍵值對的值
雜湊物件同時滿足以下兩個條件時,雜湊物件使用 ziplist 編碼:
- 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於 64 位元組
- 雜湊物件儲存的鍵值對數量小於 512個
7.5 集合物件
集合物件的編碼可以是 intset 或者 hashtable
- inset 編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合裡面
- hashtable 編碼的集合物件使用字典作為底層實現,字典的每個鍵都是一個字串物件,每個字串物件包含一個集合元素,而字典的值則全部被設定為 NULL
當集合物件可以同時滿足以下兩個條件時,物件使用 intset 編碼:
- 集合物件儲存的所有元素都是整數值
- 集合物件儲存的元素數量不超過 512 個
7.6 有序集合物件
有序集合的編碼可以是 ziplist 或者 skiplist
- ziplist 編碼的壓縮列表物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一個的壓縮列表節點來儲存,第一個節點儲存元素的成員,第二個節點儲存元素的分值,元素按照分值從小到大進行排序
- skiplist 編碼的有序集合物件使用 zset 結構作為底層實現,一個zset 結構同時包含一個字典和一個跳躍表
有序集合物件同時滿足以下兩個條件使用 ziplist 編碼:
- 有序集合儲存的元素數量小於 128 個
- 有序集合儲存的所有元素成員的長度都小於 64 位元組
7.7 檢查型別
任何型別的鍵都可以執行的命令:
DEL、EXPIRE、RENAME、TYPE、OBJECT
只能對特定型別的鍵執行:
鍵 | 命令 |
---|---|
字串鍵 | set、get、append、strlen |
雜湊鍵 | hdel、hset、hget、hlen |
列表鍵 | rpush、lpop、linsert、llen |
集合鍵 | sadd、spop、sinter、scard |
有序集合鍵 | zadd、zcard、zrank、zscore |
7.8 記憶體
記憶體回收
Redis 構建了一個引用計數計數實現的記憶體回收機制。
typedef struct redisObject {
// 引用計數
int refcount;
}robj;
- 建立一個新物件,引用計數值被初始化為 1
- 當一個物件被一個新程式使用時,它的引用計數值會被加 1
- 當一個物件不在被一個程式使用時,它的應用計數值 減 1
- 當物件引用計數值變為 0,物件所佔用的記憶體會被釋放
物件共享
Redis 讓多個鍵共享同一個值物件需要執行的步驟為:
- 將資料庫鍵的值指標指向一個現有的值物件
- 將被共享的值物件的應用計數+1