Redis 有一種底層數據結構,叫壓縮列表(ziplist),這是一種非常節省內存的結構。使用到壓縮列表的數據類型有(List,Hash,Sorted Set)
壓縮列表的構成。表頭有三個字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量,以及列表中的 entry 個數。壓縮列表尾還有一個 zlend,表示列表結束。
壓縮列表之所以能節省內存,就在于它是用一系列連續的 entry 保存數據。每個 entry 的元數據包括下面幾部分。
1,prev_len,表示前一個 entry 的長度。(為了倒序取數據方便)
2,len:表示自身長度,4 字節;
3,encoding:表示編碼方式,1 字節;
4,content:保存實際數據。
這些 entry 會挨個兒放置在內存中,不需要再用額外的指針進行連接,這樣就可以節省指針所占用的空間。
Redis 基于壓縮列表最大好處就是節省了 dictEntry 的開銷。當你用 String 類型時,一個鍵值對就有一個 dictEntry,要用 32 字節空間。但采用集合類型時,一個 key 就對應一個集合的數據,能保存的數據多了很多,但也只用了一個 dictEntry,這樣就節省了內存。
如何用集合類型保存單值的鍵值對
在保存單值的鍵值對時,可以采用基于 Hash 類型的二級編碼方法。
以圖片 ID 1101000060 和圖片存儲對象 ID 3302000080 為例,我們可以把圖片 ID 的前 7 位(1101000)作為 Hash 類型的鍵,把圖片 ID 的最后 3 位(060)和圖片存儲對象 ID 分別作為 Hash 類型值中的 key 和 value。
127.0.0.1:6379> info memory
# Memory
used_memory:1039120
127.0.0.1:6379> hset 1101000 060 3302000080
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039136
在使用 String 類型時,每個記錄需要消耗 64 字節,這種方式卻只用了 16 字節,所使用的內存空間是原來的 1/4,滿足了我們節省內存空間的需求。
Hash 類型底層結構什么時候使用壓縮列表?
其實,Hash 類型設置了用壓縮列表保存數據時的兩個閾值,一旦超過了閾值,Hash 類型就會用哈希表來保存數據了。
這兩個閾值分別對應以下兩個配置項:
hash-max-ziplist-entries:表示用壓縮列表保存時哈希集合中的最大元素個數。
hash-max-ziplist-value:表示用壓縮列表保存時哈希集合中單個元素的最大長度。
為了能充分使用壓縮列表的精簡內存布局,我們一般要控制保存在 Hash 集合中的元素個數。所以,在剛才的二級編碼中,我們只用圖片 ID 最后 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數不超過 1000,同時,我們把 hash-max-ziplist-entries 設置為 1000,這樣一來,Hash 集合就可以一直使用壓縮列表來節省內存空間了。
不管是使用Hash還是Sorted Set,當采用ziplist方式存儲時,雖然可以節省內存空間,但是在查詢指定元素時,都要遍歷整個ziplist,找到指定的元素。所以使用ziplist方式存儲時,雖然可以利用CPU高速緩存,但也不適合存儲過多的數據(hash-max-ziplist-entries和zset-max-ziplist-entries不宜設置過大),否則查詢性能就會下降比較厲害。整體來說,這樣的方案就是時間換空間,我們需要權衡使用。
當使用ziplist存儲時,盡量選擇占用內存小的方式存儲,因為ziplist是每個元素緊湊排列,而且每個元素存儲了上一個元素的長度,所以當修改其中一個元素超過一定大小時,會引發多個元素的級聯調整(前面一個元素發生大的變動,后面的元素都要重新排列位置,重新分配內存),這也會引發性能問題,需要注意。
使用Hash和Sorted Set存儲時,雖然節省了內存空間,但是設置過期變得困難(無法控制每個元素的過期,只能整個key設置過期,或者業務層單獨維護每個元素過期刪除的邏輯,但比較復雜)。而使用String雖然占用內存多,但是每個key都可以單獨設置過期時間,還可以設置maxmemory和淘汰策略,以這種方式控制整個實例的內存上限。