閒暇之餘,通讀了《Redis 設計與實現》,個人比較喜歡第一版,小記幾筆,以便查閱,如果單純爲了使用,請移步:《命令查詢手冊》,共勉~
整數集合
Intset是集合鍵的底層實現之一,用於有序、無重複的保存多個整數值,如果一個集合滿足:
- 只保存整數元素;
- 元素的數量不多;
那麼Redis就會使用intset來保存集合,我們先來看看intset的定義:
typedef struct intset {
// 保存元素所使用的類型的長度
uint32_t encoding;
// 元素個數
uint32_t length;
// 保存元素的數組
int8_t contents[];
} intset;
這個int8_t很有迷惑性,實際上,這個類型聲明只是作爲一個佔位符使用,Redis會根據元素的值,自動選擇採用什麼長度的整數類型來保存元素,比如:在一個intset裏,最長元素可以用int16_6來保存,那麼這個intset的所有元素都以int16_t類型保存,如果這時插入了一個新元素且這個元素無法用int16_t類型保存,而需要更大的int32_t來保存,那麼這個intset就會自動進行“升級”,將所有的元素從int16_t轉換爲int32_t,再將新元素插入到集合中。在對contents中的元素進行讀取或寫入時,程序並不是直接使用contents來對元素進行索引,而是根據encoding的值,對contents進行類型轉換和指針運算,計算出元素在內存中的正確位置。在添加新元素,進行內存分配時,分配的空間也由encoding的值決定。
一般來說,在做intsetAdd時,只需要保證數組中沒有重複元素且數組中的元素從小到大排列即可,不過一旦intset編碼需要“升級”時,事情似乎變得複雜了,在升級前,我們需要先明確兩點:
- 在C語言中,從長度較短的帶符號整數到長度較長的帶符號整數之間的轉換時無損的,並不會修改元素中原有的值;
- 集合編碼元素的值,由元素中長度最大的那個值決定;
如果需要升級,Redis便會將升級集合和添加新元素的任務轉交給intsetUpgradeAndAdd來完成,有以下幾步:
- 檢測新元素需要什麼類型來保存;
- 將encoding設置爲新的編碼類型,並根據新編碼類型,對整個contents數組進行內存重分配;
- 調整contents數組內原有元素在內存中的排列方式,從舊編碼調整爲新編碼;
- 將新元素添加到集合中。
整個過程,第三部最爲複雜,書中舉了個例子來方便理解它:
-
將 encoding 屬性設置爲 INTSET_ENC_INT32 。
-
根據 encoding 屬性的值,對 contents 數組進行內存重分配。
重分配完成之後, contents 在內存中的排列如下:
bit 0 15 31 47 63 95 127
value | 1 | 2 | 3 | ? | ? | ? |
contents 數組現在共有可容納 4 個 int32_t 值的空間。
因爲原來的 3 個 int16_t 值還“擠在” contents 前面的 48 個位裏, 所以程序需要移動它們並轉換類型, 讓它們適應集合的新編碼方式。
首先是移動 3 :
bit 0 15 31 47 63 95 127
value | 1 | 2 | 3 | ? | 3 | ? |
| ^
| |
+-------------+
int16_t -> int32_t
接着移動 2 :
bit 0 15 31 47 63 95 127
value | 1 | 2 | 2 | 3 | ? |
| ^
| |
+-------+
int16_t -> int32_t
最後,移動 1 :
bit 0 15 31 47 63 95 127
value | 1 | 2 | 3 | ? |
| ^
V |
int16_t -> int32_t
最後,將新值 65535 添加到數組:
bit 0 15 31 47 63 95 127
value | 1 | 2 | 3 | 65535 |
^
|
add
將 intset->length 設置爲 4 。
至此,集合的升級和添加操作完成,現在的 intset 結構如下:
intset->encoding = INTSET_ENC_INT32;
intset->length = 4;
intset->contents = [1, 2, 3, 65535];
在升級時,需要對元素進行“類型轉換”和“移動”操作,但移動不僅出現在升級中,對contents進行增刪操作一樣需要移動intset中的元素,所以我們可以看到工具手冊中集合的操作,其時間複雜度並都不低於O(N),關於Redis的集合對象,會在後文中說明。
壓縮列表
Ziplist是Redis自己實現的一種包含多個節點(entry)類似於數組數據存儲結構,每個節點可以保存一個長度受限的字符數組或整數,與數組不同的是,它允許存儲大小不同的數據。既然能稱爲壓縮列表,那麼其本身的設計目的之一肯定是爲了節省內存,也因爲這種特性,哈希鍵、列表鍵和有序集合鍵初始化的底層實現均採用了ziplist,讓我們來先看看壓縮列表的組成結構:
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte
+---------+--------+-------+--------+--------+--------+--------+-------+
component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
+---------+--------+-------+--------+--------+--------+--------+-------+
^ ^ ^
address | | |
ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END
|
ZIPLIST_ENTRY_TAIL
因爲數組爲了保證空間的連續性,存儲的大小相對固定,所以存儲時肯定會浪費部分存儲空間,所以即爲了保證連續性,又爲了節省空間,Redis考慮對數組進行壓縮,但是這種壓縮就導致一個問題,在遍歷元素時,因爲不知道元素的大小,所以無法計算出下一個節點的具體位置,所以Redis需要考慮爲每個節點增加一個length屬性,同時爲了兼顧一些其他操作做出了優化,其列表結構如下:
- zlbytes:uint32_t類型,長度4byte,用於記錄整個壓縮列表佔用的內存字節數,當壓縮列表需要進行內存重分配或者計算zlend的位置時被使用;
- zltail:unit32_t類型,長度4byte,用於記錄表尾節點距離壓縮列表起始地址有多少字節,可以通過這個寄點直接確定表位節點的地址;
- zllen:unit16_t類型,長度2byte,用於記錄壓縮列表包含的節點數量,但是當這個值等於uint16_max(65535)時,就需要遍歷整個壓縮列表才能計算出節點的真實數量;
- entry[]:節點列表,壓縮列表中的節點,其長度由節點中保存的內容決定,每個節點的構成如下:
area |<----------------- entry ------------->|
+------------------+--------------------+
component | pre_entry_length | encoding | content |
+------------------+----------+---------+
previous_entry_length屬性記錄了壓縮列表中前一個節點的長度,如果前一個節點的長度小於254字節,那麼該屬性的長度就爲1字節,如果大於或等於254字節,那麼其長度就爲5字節。content用於保存節點的值,可以是一個字節數組或整數,其類型和長度由節點encoding決定。
- zlend:uint8_t類型,長度1byte,是一個特殊值0xFF(十進制中的255),用於標記壓縮列表的末端。