1. 程式人生 > 實用技巧 >Redis-請慎用String型別

Redis-請慎用String型別

慎用 String?

開篇之前先給出一個組對比:

1. 這是執行 flushdb 後的乾淨的 Redis 記憶體使用資訊。

127.0.0.1:6379> info memory
# Memory
used_memory:502272
used_memory_human:490.50K
used_memory_rss:7901184
used_memory_peak:119628904
used_memory_peak_human:114.09M
used_memory_lua:33792
mem_fragmentation_ratio:15.73
mem_allocator:jemalloc-3.6.0

2. 執行100000次(一百萬)迴圈執行 set 操作。

// 程式碼僅作參考
$redisHandle = Redis::connection('intranet_log_redis');
for ($i= 0; $i < 1000000; $i++) {
    $redisHandle->set($i, 10000000);
}

// 執行步驟2後的記憶體資訊。
127.0.0.1:6379> info memory
# Memory
used_memory:72891064
used_memory_human:69.51M
used_memory_rss:80601088
used_memory_peak:134217744
used_memory_peak_human:128.00M
used_memory_lua:33792
mem_fragmentation_ratio:1.11
mem_allocator:jemalloc-3.6.0

// 由上資訊可知除去本身redis佔用的記憶體,一百萬個鍵值對使用了69M的記憶體。

3. 再次執行 flushdb 後,改為hash型別儲存,再次執行1000000(一百萬)次迴圈。

// 程式碼僅供參考
$redisHandle = Redis::connection('intranet_log_redis');
for ($i= 0; $i < 1000000; $i++) {
    $redisHandle->hset('testKey', $i, 10000000);
}

// 執行步驟3後檢視記憶體資訊
127.0.0.1:6379> info memory
# Memory
used_memory:72883104
used_memory_human:3.43M
used_memory_rss:82509824
used_memory_peak:134217744
used_memory_peak_human:128.00M
used_memory_lua:33792
mem_fragmentation_ratio:1.13
mem_allocator:jemalloc-3.6.0

噹噹噹! 使用了hash儲存,記憶體使用減少了 20 倍!!!這是為什麼尼???
String 型別儲存結構

為什麼儲存同樣資料量的相同資料,string 會比 hash 大了 20 倍?帶著這個疑問然後學習下 string 的具體實現方式。

上面的例子中 value 部分都是 1000000 ,可用用1個8位元組的 Long 型別儲存。而key 則是 0~1000000 同樣可以用 Long 儲存。理論上儲存只需要幾M的記憶體,為什麼用到了69M?

這就設計到 string 的編碼和結構體了,比如說64位系統中儲存一個整數。 redis 會用一個 8 位元組的 Long 型別儲存,這就是 int 編碼方式。

而字串則不太一樣,採用 SDS(Simple Dynamic String) 簡單動態字串結構體儲存。

結構 大小 描述
len 4B 4個位元組,表示 buf 已用長度
alloc 4B 4個位元組,表示 buf 的實際分配長度,通常大於 len
buf 陣列,保持實際資料。 自動在陣列後面連線一個 “\0”,標誌資料的結尾。佔1個位元組

從上表格中可以看出 SDS 會有額外的 len 和 alloc 的儲存開銷,這個有點類似 Mogodb 。當然對於 string 型別來說除了 SDS 的開銷外還有 RedisObject 結構體。它的作用在於用來記錄資料的元資料同時指向這些資料,

RedisObject 包含一個8位元組的元資料和一個8位元組的指標(真實資料位置)。當然了為了節省記憶體空間,Redis 還對 Long 型別整數和 SDS 的記憶體佈局做了專門的設計。

比如說:當儲存的是 Long 型別整數時,RedisObject 中的指標就直接賦值為整數資料了,這樣就不用額外的指標再指向整數了,節省了指標的空間開銷。

當儲存的是字串資料,並且字串小於等於 44 位元組時,RedisObject 中的元資料、指標和 SDS 是一塊連續的記憶體區域,這樣就可以避免記憶體碎片。這種佈局方式也被稱為 embstr 編碼方式。
當字串大於 44 位元組時,SDS 的資料量就開始變多了,Redis 就不再把 SDS 和 RedisObject 佈局在一起了,而是會給 SDS 分配獨立的空間,並用指標指向 SDS 結構。這種佈局方式被稱為 raw 編碼模式。

上面列舉的是 string 結構體和 RedisObject 帶來的額外儲存開銷,但是不僅僅只有這一點。Redis 會使用一個全域性雜湊表儲存所有鍵值對,雜湊表的每一項是一個 dictEntry 的結構體,用來指向一個鍵值對。dictEntry 結構中有三個 8 位元組的指標,分別指向 key、value 以及下一個 dictEntry,三個指標共 24 位元組,如下圖所示:

上面的三個指標共用了24位元組,但是實際上 Redis 會給他分配32個位元組,這個和 Redis 使用的記憶體分配 jemalloc 有關。jemalloc 在分配記憶體時,會根據我們申請的位元組數 N,找一個比 N 大,但是最接近 N 的 2 的冪次數作為分配的空間,這樣可以減少頻繁分配的次數。所以接近24且比24大的只有32。

所以上面兩個原因就是主要導致儲存一百萬個數據是使用了 69M,因為有很多地方帶來了額外的記憶體開銷。那為什麼用 hash 就小很多尼?

雜湊儲存

Redis 有一種底層資料結構,叫壓縮列表(ziplist),這是一種非常節省記憶體的結構。壓縮列表 表頭有三個欄位 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量,以及列表中的 entry 個數。壓縮列表尾還有一個 zlend,表示列表結束。如圖:

壓縮列表之所以能節省記憶體,就在於它是用一系列連續的 entry 儲存資料。不用額外的指標進行連線,所以節省空間。 每個 entry 的元資料包括下面幾部分。

結構 描述
prev_len ,表示前一個 entry 的長度。prev_len 有兩種取值情況:1 位元組或 5 位元組。取值 1 位元組時,表示上一個 entry 的長度小於 254 位元組。雖然 1 位元組的值能表示的數值範圍是 0 到 255,但是壓縮列表中 zlend 的取值預設是 255,因此,就預設用 255 表示整個壓縮列表的結束,其他表示長度的地方就不能再用 255 這個值了。所以,當上一個 entry 長度小於 254 位元組時,prev_len 取值為 1 位元組,否則,就取值為 5 位元組。
len 自身長度 4位元組
encoding 編碼方式 1位元組
content 儲存實際資料

當然了Hash 型別的兩種底層實現結構,分別是壓縮列表和雜湊表。由下面兩組引數控制合適使用雜湊表或壓縮列表。
hash-max-ziplist-entries:表示用壓縮列表儲存時雜湊集合中的最大元素個數。預設512。
hash-max-ziplist-value:表示用壓縮列表儲存時雜湊集合中單個元素的最大長度。預設60。

當 Hash 集合中寫入的元素個數超過了 hash-max-ziplist-entries,或者寫入的單個元素大小超過了 hash-max-ziplist-value,Redis 就會自動把 Hash 型別的實現結構由壓縮列表轉為雜湊表。一旦從壓縮列表轉為了雜湊表,Hash 型別就會一直用雜湊表進行儲存,而不會再轉回壓縮列表了。在節省記憶體空間方面,雜湊表就沒有壓縮列表那麼高效了。

所在文章前面做實驗時候我把 hash-max-ziplist-entries 調到了一百萬。所以但儲存大量的鍵值對時候需要謹慎些,多想想。比如可以用 Hash 型別的二級編碼方式。