跟著大彬讀原始碼 - Redis 6 - 物件和資料型別(下)
繼續擼我們的物件和資料型別。
上節我們一起認識了字串和列表,接下來還有雜湊、集合和有序集合。
1 雜湊物件
雜湊物件的可選編碼分別是:ziplist 和 hashtable。
1.1 ziplist 編碼的雜湊物件
ziplist 編碼的雜湊物件使用壓縮列表作為底層實現。每當有新的鍵值對要加入到雜湊物件時,程式會先將儲存了鍵的壓縮列表節點推入到表尾,然後再將儲存了值的壓縮列表節點推入到表尾。因此:
- 儲存了鍵值對的兩個節點總是緊挨在一起,儲存鍵的節點在前,儲存值的節點在後;
- 先新增到雜湊物件中的鍵值對會被仿造壓縮列表的表頭方向,後新增的鍵值對會被放在壓縮列表的表尾方向。
執行以下 HSET 命令,伺服器將建立一個如圖 9 所示的列表物件作為 profile 鍵的值:
127.0.0.1:6379> HSET profile name "Tom"
(integer) 1
127.0.0.1:6379> HSET profile age "25"
(integer) 1
127.0.0.1:6379> HSET profile career "Programer"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING profile
"ziplist"
其中物件所使用的壓縮列表如圖 10 所示:
1.2 hashtable 編碼的
hashtable 編碼的雜湊物件使用字典作為底層實現。雜湊物件中的每個鍵值對都使用一個字典鍵值對來儲存:
- 字典中的每個鍵都是一個字串物件,物件中儲存了鍵值對的鍵;
- 字典中的每個值都是一個字串物件,物件中儲存了鍵值對的值。
如果前面的 profile 鍵使用的是 hashtable 編碼的雜湊物件,那麼這個雜湊物件應該如圖 11 所示:
1.3 編碼轉換
當雜湊物件同時符合下面兩個條件時,將使用 ziplist 編碼:
- 雜湊物件儲存的所有鍵值對中,鍵和值的字串長度都小於 64 個位元組;
- 雜湊物件儲存的鍵值對數量小於 512 個。
上述條件中的臨界值對應 redis.conf 檔案中的配置:hash-max-ziplist-value
和 hash-max-ziplist-entries
在 3.2 版本中,新增一個雜湊鍵值對時,實際上總是先建立一個 ziplist 編碼的雜湊物件,然後再進行轉換檢查。
關於何時進行編碼轉換,有兩種情況發生:
- 更新或新增鍵值對時,如果值的位元組數大於
hash-max-ziplist-value
,將從 ziplist 編碼轉成 hashtable 編碼; - 新增鍵值對時,如果雜湊中的鍵值對數量大於
hash-max-ziplist-entries
,將從 ziplist 編碼轉成 hashtable 編碼。
要注意的是,上述發生轉換的情況,都不會出現從 hashtable 轉成 ziplist 的情況,即使符合條件。
關於雜湊編碼轉換的函式,可以參考 t_hash.c/hashTypeConvert,原始碼如下:
# o 是原始物件,enc 是目標編碼。
void hashTypeConvert(robj *o, int enc) {
if (o->encoding == OBJ_ENCODING_ZIPLIST) { // 原始編碼是 OBJ_ENCODING_ZIPLIST 才進行轉換
hashTypeConvertZiplist(o, enc);
} else if (o->encoding == OBJ_ENCODING_HT) {
serverPanic("Not implemented");
} else {
serverPanic("Unknown hash encoding");
}
}
2 集合物件
集合物件的可選編碼有:intset 和 hashtable。
2.1 intset 編碼的集合物件
intset 編碼的集合物件使用整數集合作為底層實現,集合物件包含的所有元素都被儲存在整數集合裡面。
執行以下 SADD 命令,將建立一個如圖 12 所示的 intset 編碼的集合物件:
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
2.2 hashtable 編碼的集合物件
hashtable 編碼的集合物件使用字典作為底層實現,字典的每個鍵都是一個字串物件,每個字串物件中又包含了一個集合元素,而字典的值則全部設定為 NULL。
執行以下 SADD 命令,將建立一個如圖 13 所示的 hashtable 編碼的集合物件:
127.0.0.1:6379> SADD fruits "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING fruits
"hashtable"
2.3 編碼轉換
當集合物件同時滿足以下兩個條件時,物件使用 intset 編碼:
- 集合物件儲存的所有元素都是可以被 long double 表示整數值;
- 集合物件儲存的元素數量不超過 512 個。
上述條件中的臨界值對應 redis.conf 檔案中的配置:set-max-intset-entries
。
對於集合物件,在新增第一個鍵值對時,就會對鍵值對中的值進行檢查,如果是符合條件的整數值,就會建立一個 intset 編碼的集合物件,否則,則建立 hashtable 編碼的集合物件。
關於何時進行編碼轉換,有兩種情況發生:
- 更新或新增鍵值對時,如果值不能用 long double 表示,將從 intset 編碼轉成 hashtable 編碼;
- 新增鍵值對時,如果集合中的鍵值對數量大於
set-max-intset-entries
,將從 intset 編碼轉成 hashtable 編碼。
同樣,上述發生轉換的情況,都不會出現從 hashtable 轉成 intset 的情況,即使符合條件。
關於雜湊編碼轉換的函式,可以參考 t_set.c/setTypeConvert,原始碼如下:
# setobj 是原始物件,enc 是目標編碼。
hvoid setTypeConvert(robj *setobj, int enc) {
setTypeIterator *si;
serverAssertWithInfo(NULL,setobj,setobj->type == OBJ_SET && setobj->encoding == OBJ_ENCODING_INTSET);
if (enc == OBJ_ENCODING_HT) { // 只能轉成 OBJ_ENCODING_HT 編碼
int64_t intele;
dict *d = dictCreate(&setDictType,NULL);
robj *element;
/* Presize the dict to avoid rehashing */
dictExpand(d,intsetLen(setobj->ptr));
/* To add the elements we extract integers and create redis objects */
si = setTypeInitIterator(setobj);
while (setTypeNext(si,&element,&intele) != -1) {
element = createStringObjectFromLongLong(intele);
serverAssertWithInfo(NULL,element,
dictAdd(d,element,NULL) == DICT_OK);
}
setTypeReleaseIterator(si);
setobj->encoding = OBJ_ENCODING_HT;
zfree(setobj->ptr);
setobj->ptr = d;
} else {
serverPanic("Unsupported set conversion");
}
}
3 有序集合物件
有序集合物件的可選編碼有:ziplist 和 skiplist。
3.1 ziplist 編碼的有序集合物件
intset 編碼的集合物件使用壓縮列表作為底層實現。每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存。第一個節點儲存元素的成員(member),第二個成員儲存元素的分值(score)。
壓縮列表內的集合元素按分值從小到大排序,分值較小的元素被放置在表頭的方向,而分值較大的元素則被放置在靠近表尾的方向。
執行以下 SADD 命令,將建立一個如圖 14 所示的 ziplist 編碼的集合物件:
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> OBJECT ENCODING price
"ziplist"
底層結構 ziplist 如圖 15 所示:
3.2 skiplist 編碼的集合物件
skiplist 編碼的集合物件使用 zset 作為底層實現。一個 zset 結構同時包含一個字典和一個跳躍表。結構原始碼如下:
# server.h
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
zset 結構中的 zsl 跳躍表按分值從小到大儲存了所有集合元素,每個跳躍表節點都儲存了一個集合元素。
跳躍表節點的 object 屬性儲存了元素的成員,而跳躍表節點的 score 屬性則儲存了元素的分支。**程式通過這個跳躍表,對有序集合進行範圍型操作。比如 ZRANK、ZRANGE 等命令就是基於跳躍表 API 來實現的。
除此之外,zset 結構中的 dict 字典為有序集合建立了一個從成員到分值的對映。字典中的每個鍵值對都儲存了一個集合元素:字典中的鍵儲存了元素的成員,而字典的值則儲存了元素的分值。通過這個字典,程式用 O(1) 複雜度查詢給定成員的分值。
有序集合每個元素的成員都是一個字串物件,而每個元素的分值都是一個 double 型別的浮點數。值得一提的是,雖然 zset 結構同時使用跳躍表和字典儲存了有序集合的元素,但這兩種資料結構都會通過指標來共享相同元素的成員和分值,所以不會產生任何重複成員和分值,也不會因此而浪費額外的記憶體。
如果前面 price 鍵建立的不是 ziplist 編碼的有序集合物件,而是 skiplist 編碼,那麼這個有序集合物件將會如圖 16 所示,而物件所使用的 zset 結果將會如圖 17 所示:
圖 17 中,為了展示方便,重複展示了各個元素的成員和分值。實際上,它們是共享元素的成員和分值。
3.3 編碼轉換
當有序集合物件同時滿足以下兩個條件時,物件使用 ziplist 編碼:
- 有序集合物件儲存的元素數量不超過 128 個。
- 有序集合中儲存的所有元素成員的長度都小於 64 個位元組。
上述條件中的臨界值對應 redis.conf 檔案中的配置:zset-max-ziplist-entries
和 zset-max-ziplist-value
。
對於集合物件,在新增鍵值對時,就會對集合元素以及鍵值對中的值進行檢查,如果是符合條件,就會建立一個 ziplist 編碼的集合物件,否則,則建立 skiplist 編碼的集合物件。對應原始碼如下:
# t_zset.c/zaddGenericCommand
...
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
# 物件元素數量為 0,或者
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
} else {
if (zobj->type != OBJ_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}
總結
- 雜湊物件有 ziplist 和 hashtable 編碼。
- 集合物件有 intset 和 hashtable 編碼。
- 有序集合物件有 ziplist 和 skiplist 編碼。