Redis
是一個鍵值對數據庫,其鍵是經過哈希進行存儲的。整個 Redis
能夠認爲是一個外層哈希,之因此稱爲外層哈希,是由於 Redis
內部也提供了一種哈希類型,這個能夠稱之爲內部哈希。當咱們採用哈希對象進行數據存儲時,對整個 Redis
而言,就通過了兩層哈希存儲。html
哈希對象自己也是一個 key-value
存儲結構,底層的存儲結構也能夠分爲兩種:ziplist
(壓縮列表) 和 hashtable
(哈希表)。這兩種存儲結構也是經過編碼來進行區分:java
編碼屬性 | 描述 | object encoding命令返回值 |
---|---|---|
OBJ_ENCODING_ZIPLIST | 使用壓縮列表實現哈希對象 | ziplist |
OBJ_ENCODING_HT | 使用字典實現哈希對象 | hashtable |
Redis
中的 key-value
是經過 dictEntry
對象進行包裝的,而哈希表就是將 dictEntry
對象又進行了再一次的包裝獲得的,這就是哈希表對象 dictht
:數據庫
typedef struct dictht { dictEntry **table;//哈希表數組 unsigned long size;//哈希表大小 unsigned long sizemask;//掩碼大小,用於計算索引值,老是等於size-1 unsigned long used;//哈希表中的已有節點數 } dictht;
注意:上面結構定義中的 table
是一個數組,其每一個元素都是一個 dictEntry
對象。數組
字典,又稱爲符號表(symbol table),關聯數組(associative array)或者映射(map),字典的內部嵌套了哈希表 dictht
對象,下面就是一個字典 ht
的定義:安全
typedef struct dict { dictType *type;//字典類型的一些特定函數 void *privdata;//私有數據,type中的特定函數可能須要用到 dictht ht[2];//哈希表(注意這裏有2個哈希表) long rehashidx; //rehash索引,不在rehash時,值爲-1 unsigned long iterators; //正在使用的迭代器數量 } dict;
其中 dictType
內部定義了一些經常使用函數,其數據結構定義以下:服務器
typedef struct dictType { uint64_t (*hashFunction)(const void *key);//計算哈希值函數 void *(*keyDup)(void *privdata, const void *key);//複製鍵函數 void *(*valDup)(void *privdata, const void *obj);//複製值函數 int (*keyCompare)(void *privdata, const void *key1, const void *key2);//對比鍵函數 void (*keyDestructor)(void *privdata, void *key);//銷燬鍵函數 void (*valDestructor)(void *privdata, void *obj);//銷燬值函數 } dictType;
當咱們建立一個哈希對象時,能夠獲得以下簡圖(部分屬性被省略):數據結構
dict
中定義了一個數組 ht[2]
,ht[2]
中定義了兩個哈希表:ht[0]
和 ht[1]
。而 Redis
在默認狀況下只會使用 ht[0]
,並不會使用 ht[1]
,也不會爲 ht[1]
初始化分配空間。函數
當設置一個哈希對象時,具體會落到哈希數組(上圖中的 dictEntry[3]
)中的哪一個下標,是經過計算哈希值來肯定的。若是發生哈希碰撞(計算獲得的哈希值一致),那麼同一個下標就會有多個 dictEntry
,從而造成一個鏈表(上圖中最右邊指向 NULL
的位置),不過須要注意的是最後插入元素的老是落在鏈表的最前面(即發生哈希衝突時,老是將節點往鏈表的頭部放)。性能
當讀取數據的時候遇到一個節點有多個元素,就須要遍歷鏈表,故鏈表越長,性能越差。爲了保證哈希表的性能,須要在知足如下兩個條件中的一個時,對哈希表進行 rehash
(從新散列)操做:測試
1
且 dict_can_resize
爲 1
時。dict_force_resize_ratio=5
)時。PS:負載因子 = 哈希表已使用節點數 / 哈希表大小(即:h[0].used/h[0].size
)。
擴展哈希和收縮哈希都是經過執行 rehash
來完成,這其中就涉及到了空間的分配和釋放,主要通過如下五步:
爲字典 dict
的 ht[1]
哈希表分配空間,其大小取決於當前哈希表已保存節點數(即:ht[0].used
):
ht[1]
的大小爲 2 的
n次方中第一個大於等於
ht[0].used * 2屬性的值(好比
used=3,此時
ht[0].used * 2=6,故
2的
3次方爲
8就是第一個大於
used * 2 的值(2 的 2 次方 < 6 且 2 的 3 次方 > 6))。
ht[1]
大小爲 2 的 n 次方中第一個大於等於 ht[0].used
的值。將字典中的屬性 rehashix
的值設置爲 0
,表示正在執行 rehash
操做。
將 ht[0]
中全部的鍵值對依次從新計算哈希值,並放到 ht[1]
數組對應位置,每完成一個鍵值對的 rehash
以後 rehashix
的值須要自增 1
。
當 ht[0]
中全部的鍵值對都遷移到 ht[1]
以後,釋放 ht[0]
,並將 ht[1]
修改成 ht[0]
,而後再建立一個新的 ht[1]
數組,爲下一次 rehash
作準備。
將字典中的屬性 rehashix
設置爲 -1
,表示這次 rehash
操做結束,等待下一次 rehash
。
Redis
中的這種從新哈希的操做由於不是一次性所有 rehash
,而是分屢次來慢慢的將 ht[0]
中的鍵值對 rehash
到 ht[1]
,故而這種操做也稱之爲漸進式 rehash
。漸進式 rehash
能夠避免集中式 rehash
帶來的龐大計算量,是一種分而治之的思想。
在漸進式 rehash
過程當中,由於還可能會有新的鍵值對存進來,此時** Redis
的作法是新添加的鍵值對統一放入 ht[1]
中,這樣就確保了 ht[0]
鍵值對的數量只會減小**。
當正在執行 rehash
操做時,若是服務器收到來自客戶端的命令請求操做,則會先查詢 ht[0]
,查找不到結果再到ht[1]
中查詢。
關於 ziplist
的一些特性,以前的文章中有單獨進行過度析,想要詳細瞭解的,能夠點擊這裏。可是須要注意的是哈希對象中的 ziplist
和列表對象中 ziplist
的有一點不一樣就是哈希對象是一個 key-value
形式,因此其 ziplist
中也表現爲 key-value
,key
和 value
緊挨在一塊兒:
當一個哈希對象能夠知足如下兩個條件中的任意一個,哈希對象會選擇使用 ziplist
編碼來進行存儲:
64
字節(這個閾值能夠經過參數 hash-max-ziplist-value
來進行控制)。512
個(這個閾值能夠經過參數 hash-max-ziplist-entries
來進行控制)。一旦不知足這兩個條件中的任意一個,哈希對象就會選擇使用 hashtable
編碼進行存儲。
field
(哈希對象的 key
值)。field
(哈希對象的 key
值)。key
中域 field
的值設置爲 value
,若是 field
已存在,則不執行任何操做。key
中的域 field
對應的 value
。key
中的多個域 field
對應的 value
。key
中的一個或者多個 field
。key
中的域 field
的值加上增量 increment
,increment
能夠爲負數,若是 field
不是數字則會報錯。key
中的域 field
的值加上增量 increment
,increment
能夠爲負數,若是 field
不是 float
類型則會報錯。key
中的全部域。瞭解了操做哈希對象的經常使用命令,咱們就能夠來驗證下前面提到的哈希對象的類型和編碼了,在測試以前爲了防止其餘 key
值的干擾,咱們先執行 flushall
命令清空 Redis
數據庫。
而後依次執行以下命令:
hset address country china type address object encoding address
獲得以下效果:
能夠看到當咱們的哈希對象中只有一個鍵值對的時候,底層編碼是 ziplist
。
如今咱們將 hash-max-ziplist-entries
參數改爲 2
,而後重啓 Redis
,最後再輸入以下命令進行測試:
hmset key field1 value1 field2 value2 field3 value3 object encoding key
輸出以後獲得以下結果:
能夠看到,編碼已經變成了 hashtable
。
本文主要介紹了 Redis
中 5
種經常使用數據類型中的哈希類型底層的存儲結構 hashtable
的使用,以及當 hash
分佈不均勻時候 Redis
是如何進行從新哈希的問題,最後瞭解了哈希對象的一些經常使用命令,並經過一些例子驗證了本文的結論。