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 型別的二級編碼方式。