一、前言
Redis 提供了5種數據類型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),理解每種數據類型的特點對於redis的開發和運維非常重要。
Redis 中的 hash 是我們經常使用到的一種數據類型,根據使用方式的不同,可以應用到很多場景中。
二、實現分析
由上述結構圖可知,Hash類型有以下兩種實現方式:
1、ziplist 編碼的哈希對象使用壓縮列表作為底層實現
2、hashtable 編碼的哈希對象使用字典作為底層實現
1.ziplist 編碼作為底層實現
ziplist 編碼的哈希對象使用壓縮列表作為底層實現, 每當有新的鍵值對要加入到哈希對象時, 程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾, 然后再將保存了值的壓縮列表節點推入到壓縮列表表尾, 因此:
保存了同一鍵值對的兩個節點總是緊挨在一起, 保存鍵的節點在前, 保存值的節點在后;
先添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向,而后來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。
例如, 我們執行以下 HSET 命令, 那么服務器將創建一個列表對象作為 profile 鍵的值:
redis> HSET profile name "Tom"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1
profile 鍵的值對象使用的是 ziplist 編碼, 其中對象所使用的壓縮列表結構如下圖所示。


2.hashtable 編碼作為底層實現
hashtable 編碼的哈希對象使用字典作為底層實現, 哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:
字典的每個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵;
字典的每個值都是一個字符串對象, 對象中保存了鍵值對的值。
例如, 如果前面 profile 鍵創建的不是 ziplist 編碼的哈希對象, 而是 hashtable 編碼的哈希對象, 那么這個哈希對象結構如下圖所示。

三、命令實現
因為哈希鍵的值為哈希對象, 所以用於哈希鍵的所有命令都是針對哈希對象來構建的, 下表列出了其中一部分哈希鍵命令, 以及這些命令在不同編碼的哈希對象下的實現方法。
命令 | ziplist 編碼實現方法 | hashtable 編碼的實現方法 |
---|---|---|
HSET | 首先調用 ziplistPush 函數, 將鍵推入到壓縮列表的表尾, 然后再次調用 ziplistPush 函數, 將值推入到壓縮列表的表尾。 | 調用 dictAdd 函數, 將新節點添加到字典里面。 |
HGET | 首先調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 然后調用 ziplistNext 函數, 將指針移動到鍵節點旁邊的值節點, 最后返回值節點。 | 調用 dictFind 函數, 在字典中查找給定鍵, 然后調用dictGetVal 函數, 返回該鍵所對應的值。 |
HEXISTS | 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 如果找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。 | 調用 dictFind 函數, 在字典中查找給定鍵, 如果找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。 |
HDEL | 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 然后將相應的鍵節點、 以及鍵節點旁邊的值節點都刪除掉。 | 調用 dictDelete 函數, 將指定鍵所對應的鍵值對從字典中刪除掉。 |
HLEN | 調用 ziplistLen 函數, 取得壓縮列表包含節點的總數量, 將這個數量除以 2 , 得出的結果就是壓縮列表保存的鍵值對的數量。 | 調用 dictSize 函數, 返回字典包含的鍵值對數量, 這個數量就是哈希對象包含的鍵值對數量。 |
HGETALL | 遍歷整個壓縮列表, 用 ziplistGet 函數返回所有鍵和值(都是節點)。 | 遍歷整個字典, 用 dictGetKey 函數返回字典的鍵, 用dictGetVal 函數返回字典的值。 |
四、編碼轉換
當哈希對象可以同時滿足以下兩個條件時, 哈希對象使用 ziplist 編碼:
哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於 64 字節;
哈希對象保存的鍵值對數量小於 512 個;
不能滿足這兩個條件的哈希對象需要使用 hashtable 編碼。
注意:這兩個條件的上限值是可以修改的, 具體請看配置文件中關於 hash-max-ziplist-value 選項和 hash-max-ziplist-entries 選項的說明。
對於使用 ziplist 編碼的列表對象來說, 當使用 ziplist 編碼所需的兩個條件的任意一個不能被滿足時, 對象的編碼轉換操作就會被執行: 原本保存在壓縮列表里的所有鍵值對都會被轉移並保存到字典里面, 對象的編碼也會從 ziplist 變為 hashtable 。
以下代碼展示了哈希對象編碼轉換的情況:
1.鍵的長度太大引起編碼轉換
# 哈希對象只包含一個鍵和值都不超過 64 個字節的鍵值對
redis> HSET book name "Mastering C++ in 21 days"
(integer) 1
redis> OBJECT ENCODING book
"ziplist"
# 向哈希對象添加一個新的鍵值對,鍵的長度為 66 字節
redis> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content"
(integer) 1
# 編碼已改變
redis> OBJECT ENCODING book
"hashtable"
2.值的長度太大引起編碼轉換
# 哈希對象只包含一個鍵和值都不超過 64 個字節的鍵值對
redis> HSET blah greeting "hello world"
(integer) 1
redis> OBJECT ENCODING blah
"ziplist"
# 向哈希對象添加一個新的鍵值對,值的長度為 68 字節
redis> HSET blah story "many string ... many string ... many string ... many string ... many"
(integer) 1
# 編碼已改變
redis> OBJECT ENCODING blah
"hashtable"
3.鍵值對數量過多引起編碼轉換
# 創建一個包含 512 個鍵值對的哈希對象
redis> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i) end" 1 "numbers"
(nil)
redis> HLEN numbers
(integer) 512
redis> OBJECT ENCODING numbers
"ziplist"
# 再向哈希對象添加一個新的鍵值對,使得鍵值對的數量變成 513 個
redis> HMSET numbers "key" "value"
OK
redis> HLEN numbers
(integer) 513
# 編碼改變
redis> OBJECT ENCODING numbers
"hashtable"
五、要點總結
1.Hash類型兩種編碼方式,ziplist 與 hashtable
2.hashtable 編碼的哈希對象使用字典作為底層實現
3.ziplist 與 hashtable 編碼方式之間存在轉換