1. 程式人生 > >Redis中雜湊分佈不均勻該怎麼辦

Redis中雜湊分佈不均勻該怎麼辦

# 前言 `Redis` 是一個鍵值對資料庫,其鍵是通過雜湊進行儲存的。整個 `Redis` 可以認為是一個外層雜湊,之所以稱為外層雜湊,是因為 `Redis` 內部也提供了一種雜湊型別,這個可以稱之為內部雜湊。當我們採用雜湊物件進行資料儲存時,對整個 `Redis` 而言,就經過了兩層雜湊儲存。 # 雜湊物件 雜湊物件本身也是一個 `key-value` 儲存結構,底層的儲存結構也可以分為兩種:`ziplist`(壓縮列表) 和 `hashtable`(雜湊表)。這兩種儲存結構也是通過編碼來進行區分: | 編碼屬性 | 描述 | object encoding命令返回值 | | -------------------- | ------------------------ | ------------------------- | | OBJ_ENCODING_ZIPLIST | 使用壓縮列表實現雜湊物件 | ziplist | | OBJ_ENCODING_HT | 使用字典實現雜湊物件 | hashtable | ## hashtable `Redis` 中的 `key-value` 是通過 `dictEntry` 物件進行包裝的,而雜湊表就是將 `dictEntry` 物件又進行了再一次的包裝得到的,這就是雜湊表物件 `dictht`: ```c 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` 的定義: ```c typedef struct dict { dictType *type;//字典型別的一些特定函式 void *privdata;//私有資料,type中的特定函式可能需要用到 dictht ht[2];//雜湊表(注意這裡有2個雜湊表) long rehashidx; //rehash索引,不在rehash時,值為-1 unsigned long iterators; //正在使用的迭代器數量 } dict; ``` 其中 `dictType ` 內部定義了一些常用函式,其資料結構定義如下: ```c 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; ``` 當我們建立一個雜湊物件時,可以得到如下簡圖(部分屬性被省略): ![](https://img2020.cnblogs.com/blog/2232223/202101/2232223-20210120213440752-2100115347.png) ### rehash 操作 `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 步驟 擴充套件雜湊和收縮雜湊都是通過執行 `rehash` 來完成,這其中就涉及到了空間的分配和釋放,主要經過以下五步: 1. 為字典 `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` 的值。 2. 將字典中的屬性 `rehashix` 的值設定為 `0`,表示正在執行 `rehash` 操作。 3. 將 `ht[0]` 中所有的鍵值對依次重新計算雜湊值,並放到 `ht[1]` 陣列對應位置,每完成一個鍵值對的 `rehash`之後 `rehashix` 的值需要自增 `1`。 4. 當 `ht[0]` 中所有的鍵值對都遷移到 `ht[1]` 之後,釋放 `ht[0]` ,並將 `ht[1]` 修改為 `ht[0]`,然後再建立一個新的 `ht[1]` 陣列,為下一次 `rehash` 做準備。 5. 將字典中的屬性 `rehashix` 設定為 `-1`,表示此次 `rehash` 操作結束,等待下一次 `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` 的一些特性,之前的文章中有單獨進行過分析,想要詳細瞭解的,可以[點選這裡](https://www.cnblogs.com/lonely-wolf/p/14281136.html)。但是需要注意的是雜湊物件中的 `ziplist` 和列表物件中 `ziplist` 的有一點不同就是雜湊物件是一個 `key-value` 形式,所以其 `ziplist` 中也表現為 `key-value`,`key` 和 `value` 緊挨在一起: ![](https://img2020.cnblogs.com/blog/2232223/202101/2232223-20210120213514656-935498919.png) ## ziplist 和 hashtable 的編碼轉換 當一個雜湊物件可以滿足以下兩個條件中的任意一個,雜湊物件會選擇使用 `ziplist` 編碼來進行儲存: - 雜湊物件中的所有鍵值對總長度(包括鍵和值)小於等於 `64`位元組(這個閾值可以通過引數 `hash-max-ziplist-value` 來進行控制)。 - 雜湊物件中的鍵值對數量小於等於 `512` 個(這個閾值可以通過引數 `hash-max-ziplist-entries` 來進行控制)。 一旦不滿足這兩個條件中的任意一個,雜湊物件就會選擇使用 `hashtable` 編碼進行儲存。 ## 雜湊物件常用命令 - hset key field value:設定單個 `field`(雜湊物件的 `key` 值)。 - hmset key field1 value1 field2 value2 :設定多個 `field`(雜湊物件的 `key` 值)。 - hsetnx key field value:將雜湊表 `key` 中域 `field` 的值設定為 `value`,如果 `field` 已存在,則不執行任何操作。 - hget key field:獲取雜湊表 `key` 中的域 `field` 對應的 `value`。 - hmget key field1 field2:獲取雜湊表 `key` 中的多個域 `field` 對應的 `value`。 - hdel key field1 field2:刪除雜湊表 `key` 中的一個或者多個 `field`。 - hlen key:返回雜湊表key中域的數量。 - hincrby key field increment:為雜湊表 `key` 中的域 `field` 的值加上增量 `increment` ,`increment` 可以為負數,如果 `field` 不是數字則會報錯。 - hincrbyfloat key field increment:為雜湊表 `key` 中的域 `field` 的值加上增量 `increment`,`increment` 可以為負數,如果 `field` 不是 `float` 型別則會報錯。 - hkeys key:獲取雜湊表 `key` 中的所有域。 - hvals key:獲取雜湊表中所有域的值。 瞭解了操作雜湊物件的常用命令,我們就可以來驗證下前面提到的雜湊物件的型別和編碼了,在測試之前為了防止其他 `key` 值的干擾,我們先執行 `flushall` 命令清空 `Redis` 資料庫。 然後依次執行如下命令: ```java hset address country china type address object encoding address ``` 得到如下效果: ![](https://img2020.cnblogs.com/blog/2232223/202101/2232223-20210120213546323-1888508446.png) 可以看到當我們的雜湊物件中只有一個鍵值對的時候,底層編碼是 `ziplist`。 現在我們將 `hash-max-ziplist-entries` 引數改成 `2`,然後重啟 `Redis`,最後再輸入如下命令進行測試: ```java hmset key field1 value1 field2 value2 field3 value3 object encoding key ``` 輸出之後得到如下結果: ![](https://img2020.cnblogs.com/blog/2232223/202101/2232223-20210120213612035-857436495.png) 可以看到,編碼已經變成了 `hashtable`。 # 總結 本文主要介紹了 `Redis` 中 `5` 種常用資料型別中的雜湊型別底層的儲存結構 `hashtable` 的使用,以及當 `hash` 分佈不均勻時候 `Redis` 是如何進行重新雜湊的問題,最後瞭解了雜湊物件的一些常用命令,並通過一些例子驗證了本文的結論。