天天看點

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

開篇

之前的分享内容都是相對零散的知識點,不成體系。以後的每周分享,我會盡量将每篇文章串連起來,于是我決定做一個專欄,名字就叫《學習分享》。這是該系列的第一篇。

《學習分享》每周一或周二發表,這些内容大多來自我平時學習過程中的筆記,筆記倉庫在 Github:studeyang/technotes[1]。其中我認為有深度、對工作有幫助的内容,就會以文章的形式發表在該專欄,内容會首發在我的公衆号[2]和今日頭條[3],也會維護在 Github:studeyang/leanrning-share[4]。

回顧

上篇文章《Redis 的 String 類型,原來這麼占記憶體》中,我們使用 String 類型存儲了圖檔 ID 和圖檔存儲對象 ID,結果發現兩個 Long 類型的 ID 竟然占了 68 位元組記憶體。具體驗證過程,我還是貼一下友善你回顧。

1、檢視 Redis 的初始記憶體使用情況。

127.0.0.1:6379> info memory
# Memory
used_memory:871840           

2、接着插入 10 條資料。

10.118.32.170:0> set 1101000060 3302000080
10.118.32.170:0> set 1101000061 3302000081
10.118.32.170:0> set 1101000062 3302000082
10.118.32.170:0> set 1101000063 3302000083
10.118.32.170:0> set 1101000064 3302000084
10.118.32.170:0> set 1101000065 3302000085
10.118.32.170:0> set 1101000066 3302000086
10.118.32.170:0> set 1101000067 3302000087
10.118.32.170:0> set 1101000068 3302000088
10.118.32.170:0> set 1101000069 3302000089           

3、再次檢視記憶體。

127.0.0.1:6379> info memory
# Memory
used_memory:872528           

可以看到,存儲 10 個圖檔,記憶體使用了 688 個位元組。一個圖檔 ID 和圖檔存儲對象 ID 的記錄平均用了 68 位元組。

這是上次我們講述的場景。

并且還留下了一道思考題:既然 String 類型這麼占記憶體,那麼你有好的方案來節省記憶體嗎?

今天呢,我們就來具體談一談。

用什麼資料結構可以節省記憶體?

Redis 提供了一種非常節省記憶體的資料結構,叫壓縮清單(ziplist)。它是由一系列特殊編碼的連續記憶體塊組成的順序性(sequential)資料結構,一個壓縮清單可以包含多個節點,每個節點可以儲存一個位元組數組或者一個整數值。

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

壓縮清單各個部分含義如下。

  • zlbytes:表示壓縮清單占用的記憶體位元組數。
  • zltail:表示壓縮清單表尾節點距離起始位址有多少位元組。
  • zllen:表示壓縮清單包含的節點數量。
  • entry:壓縮清單的各個節點。
  • zlend:特殊值 0xFF (十進制 255),用于标記壓縮清單的末端。

舉個例子,壓縮清單 zlbytes 值為 0x50 (十進制是 80),表示該壓縮清單占用 80 位元組;zltail 值為 0x3c (十進制是 60),表示如果有一個指向壓縮清單起始位址的指針 p,那麼隻要用指針 p 加上偏移量 60,就可以計算出表尾節點 entry3 的位址;zllen 值為 0x3 (十進制是 3),表示壓縮清單有三個節點。

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

壓縮清單之是以能節省記憶體,就在于它是用一系列連續的 entry 儲存資料。每個 entry 的中繼資料包括下面幾部分。

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體
  • prevlen,表示前一個 entry 的長度。prev_len 有兩種取值情況:1 位元組或 5 位元組。如果上一個 entry 的長度小于 254 位元組,取值 1 位元組;否則,就取值為 5 位元組;
  • encoding:表示編碼方式,1 位元組;
  • len:表示自身長度,4 位元組;
  • data:儲存實際資料。

由于 ziplist 節省記憶體的特性,哈希鍵(Hash)、清單鍵(List)和有序集合鍵(Sorted Set)初始化的底層實作皆采用 ziplist。

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

我們先看一下能不能使用 Sorted Set 類型來進行儲存。

首先,使用 Sorted Set 類型儲存資料,面臨的第一個問題就是:在一個鍵對應一個值的情況下,我們該怎麼用集合類型來儲存這種單值鍵值對呢?

我們知道 Sorted Set 的元素有 member 值和 score 值,可以把圖檔 ID 拆成兩部分進行儲存。具體做法是,把圖檔 ID 的前 7 位作為 Sorted Set 的 key,把圖檔 ID 的後 3 位作為 member 值,圖檔存儲對象 ID 作為 score 值。

Sorted Set 中元素較少時,Redis 會使用壓縮清單進行存儲,可以節省記憶體空間。但是,在插入資料時,Sorted Set 需要按 score 值的大小進行排序,它的性能就差了。

是以,Sorted Set 類型雖然可以用來儲存圖檔 ID 和圖檔存儲對象 ID,但并不是最優選項。

那 List 類型呢?

List 類型對于存儲圖檔 ID 和圖檔存儲對象 ID 這種一對一的場景不是很适合。我們可以使用 Hash 類型。

使用 Hash 類型

還是用上面拆成兩部分儲存的方法,把圖檔 ID 的前 7 位 Hash 集合的 key,把圖檔 ID 的後 3 位作為 Hash 集合的 value。

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

對于資料 060,會選擇對應的編碼 11000000;同樣,資料 3302000080 對應的編碼是 11100000。

為什麼對應的編碼是這個?這裡不是很清楚?沒關系,這不影響你了解本文内容,如果你感興趣,可以自行檢視一下源碼。

其中有的 entry 儲存一個圖檔 ID 的後 3 位(4 位元組),有的 entry 儲存存儲對象 ID(8 位元組),此時,每個 entry 的 prev_len 隻需要 1 個位元組就行,因為每個 entry 的前一個 entry 長度都小于 254 位元組。這樣一來,一個圖檔 ID 後 3 位所占用的記憶體大小是 8 位元組(1+1+4+4);一個存儲對象 ID 所占用的記憶體大小是 14 位元組(1+1+4+8=14),實際配置設定 16 位元組。

10 個圖檔所占用的記憶體就是:ziplist 4(zlbytes) + 4(zltail) + 2(zllen) + 8*10(entry) + 16*10(entry) + 1(zlend) = 251 位元組。

結合全局哈希表,記憶體各部分占用如下:

學習分享(第 1 期)之 Redis:巧用 Hash 類型節省記憶體

10 個圖檔占 32(dictEntry) + 8(key) + 16(redisObject) + 251 = 307 位元組。

這比 String 的類型的存儲結果 688 節約了一倍的記憶體。

我們也通過下面的實戰來驗證一下。

127.0.0.1:6379> info memory
# Memory
used_memory:871872
127.0.0.1:6379> hset 1101000 060 3302000080 061 3302000081 ...
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:872152           

實際使用了 280 位元組。

不過,這裡你可能會問了,圖檔 ID 1101000060 一定要折成 7+3,即 1101000+060 的方式嗎?拆成 5+5,即 11010+00060 行不行?

一定要 7+3 的方式存儲 key 嗎?

答案是肯定的。

Redis Hash 類型的兩種底層資料結構,一種是壓縮清單,另一種是哈希表。Hash 類型設定了壓縮清單儲存資料的門檻值,一旦超過了門檻值,Hash 類型就會用哈希表來儲存資料了。

如果我們往 Hash 集合中寫入的元素個數超過了 hash-max-ziplist-entries (預設 512 個),或者寫入的單個元素大小超過了 hash-max-ziplist-value (預設 64 位元組),Redis 就會自動把 Hash 類型的實作結構由壓縮清單轉為哈希表。在節省記憶體方面,哈希表就沒有壓縮清單那麼高效了。

為了能使用壓縮清單來節省記憶體,我們一般要控制儲存在 Hash 集合中的元素個數。是以,我們隻用圖檔 ID 的後 3 位作為 Hash 集合的 key,也就保證了 Hash 集合的元素個數不超過 1000,同時,我們把 hash-max-ziplist-entries 設定為 1000,這樣一來,Hash 集合就可以一直使用壓縮清單來節省記憶體空間了。

參考資料

  • 文中的一些指令,參考菜鳥教程:https://www.runoob.com/redis/redis-tutorial.html
  • 極客時間《Redis 核心技術與實戰》
  • 書籍《Redis 設計與實作》
  • 壓縮清單:https://redisbook.readthedocs.io/en/latest/compress-datastruct/ziplist.html
  • 哈希表:http://redisbook.com/preview/object/hash.html

相關文章

也許你對下面文章也感興趣。

  • Redis高可用之哨兵機制實作細節
  • Redis高可用全景一覽
  • 現有1億個使用者10天的簽到情況,你能統計出這10天連續簽到的使用者總數嗎?

文中連結

[1] studeyang/technotes: https://github.com/studeyang/technotes

[2] 公衆号: https://mp.weixin.qq.com/s/TWRVaQPhrQf9oPxsAsuIKQ

[3] 今日頭條: https://www.toutiao.com/c/user/token/MS4wLjABAAAArFlpgpSvRI74ttxw76bAENUnFIFcYTJQnZYS77fZmNQ/?source=mp_msg&tab=article

[4] studeyang/leanrning-share: https://github.com/studeyang/learning-share

繼續閱讀