簡介
降低 Redis 的記憶體佔用有助於減少建立快照和載入快照所需的時間、提升載入 AOF 檔案和重寫 AOF 檔案時的效率、縮短從伺服器進行同步所需的時間(快照、 AOF 檔案重寫在 持久化選項 中進行了介紹,從伺服器同步在 複製、處理故障、事務及效能優化 中進行了介紹),並且能讓 Redis 儲存更多的資料而無需新增額外的硬體。 P208
短結構 (short structure) P208
Redis 為列表、集合、雜湊和有序集合提供了一組配置選項,這些選項可以讓 Redis 以更節約空間的方式儲存長度較短的結構(後面簡稱“短結構”)。 P208
在列表、雜湊和有序集合的長度較短或者體積較小的時候, Redis 可以選擇使用一種名為壓縮列表 (ziplist) 的緊湊儲存方式來儲存這些機構。壓縮列表是列表、雜湊和有序集合這 3 種不同型別的物件的一種非結構化 (unstructured) 表示:與 Redis 在通常情況下使用雙向連結串列表示列表、使用雜湊表表示雜湊、使用雜湊表加上跳錶 (skiplist) 表示有序集合的做法不同,壓縮列表會以序列化的方式儲存資料,這些序列化資料每次被讀取的時候都要進行解碼,每次被寫入的時候也要進行區域性的重新編碼,並且可能需要對記憶體裡面的資料進行移動。 P209
壓縮列表表示 P209
本節以最簡單的列表進行觀察對比。
雙向連結串列 P209
列表不進行壓縮時使用雙向連結串列 (doubly linked list) 進行儲存,連結串列的每個結點都有三個指標: P209
- 指向前一個結點的指標
- 指向後一個結點的指標
- 指向結點包含的字串值的指標
其中字串值又分為三個部分: P209
- 字串的長度
- 字串剩餘可用的位元組數
- 以空字元結尾的字串本身
可以發現未壓縮前,每儲存一個字串,最少需要 21 位元組的額外開銷 (overhead) 。(三個指標每個佔 4 個位元組,兩個整數每個佔 4 個位元組,字串結尾的空字元佔 1 個位元組) P209
壓縮列表 P209
壓縮列表是由結點(非真實結點)組成的序列 (sequence) ,每個結點都由兩個長度值和一個字串組成。 P209
- 第一個長度值:前一個結點的長度,用於從後向前的遍歷(一般以一個位元組儲存)
- 第二個長度值:當前結點的長度(一般以一個位元組儲存)
- 字串:長度等於位元組數,沒有空字元
可以發現壓縮後,每儲存一個字串,最少需要 2 位元組的額外開銷。 P210
使用壓縮列表編碼 P210
不同結構關於使用壓縮列表的配置選項 P210
# 列表使用壓縮列表表示的限制條件
list-max-ziplist-entries 512
list-max-ziplist-value 64
# 雜湊使用壓縮列表表示的限制條件
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# 有序集合使用壓縮列表表示的限制條件
zset-max-ziplist-entries 512
zset-max-ziplist-value 64
其中, ...-entries
選項說明列表、雜湊和有序集合在被編碼為壓縮列表的情況下,允許包含的最大元素數量; ...-value
選項說明了壓縮列表每個結點的最大體積是多少位元組。當這些選項設定的限制條件中的任意一個被突破時, Redis 就會將對應的列表、雜湊和有序集合從壓縮列表編碼轉換為其他結構,而記憶體佔用也會因此增加,並且即使其將來重新滿足限制條件,也不會再轉換回壓縮列表。 P210
除錯 P210
OBJECT
命令允許從內部檢視給定 key 的 Redis 物件, 它通常用在除錯(debugging) 或者瞭解為了節省空間而對 key 使用特殊編碼的情況。當將Redis 用於進行快取時,也可以通過 OBJECT
命令中的資訊,決定 key 的驅逐策略 (eviction policies) 。
OBJECT REFCOUNT <key>
: 返回給定 key 引用所儲存的值的次數。主要用於除錯OBJECT ENCODING <key>
: 返回給定 key 所儲存的值所使用的內部表示(representation)OBJECT IDLETIME <key>
: 返回給定 key 自儲存以來的空閒時間(idle, 沒有被讀取也沒有被寫入),以秒為單位
集合的整數集合編碼 P211
如果集合的所有成員都可以被解釋為十進位制整數(在平臺的有符號整數範圍內),並且集合成員的數量足夠少,那麼 Redis 就會以有序整數陣列的方式儲存集合,這種儲存方式又被稱為整數集合 (intset) 。整數集合不僅可以降低記憶體消耗,還可以提升所有標準集合操作的執行速度。 P211
整數集合的配置選項 P211
# 集合使用整數集合表示的限制條件
set-max-intset-entries 512
當整數集合包含當元素數量超過配置選項設定的限制時,整數集合將被轉換為雜湊表表示。 P212
長壓縮列表和大整數集合帶來的效能問題 P212
壓縮列表結點數 | 效能 |
---|---|
< 1000 | 差別不大 |
5000 ~ 10000 | 開始下降 |
50000 | 下降明顯 |
> 100000 | 低到無法使用 |
推薦將壓縮列表的長度限制在 1024 個元素內,並且每個元素的體積不能超過 64 位元組,對於大多數雜湊應用來說,這種配置可以同時兼顧低記憶體佔用和高效能這兩方面優點。 P214
注
Redis 在 3.2 版本後的列表底層預設使用 quicklist ,這種資料結構兼顧了雙向連結串列和壓縮列表的優點,因此列表目前來說已使用最優配置。
我們在設計 Redis 時同時也要保持鍵名簡短(包括資料鍵、雜湊的域、集合和有序集合的成員以及所有列表的結點),當儲存結點的資料量達到上百萬個或者數十億個時,將能節省 MB 升至 GB 級的空間。 P214
分片結構 P214
分片 (sharding) 本質上就是基於某些簡單的規則將資料劃分為更小的部分,然後根據資料所屬的部分來決定將資料傳送到哪個位置上面。這種技術可以擴充套件儲存空間並提高所能處理的負載量。 P214
接下來將把分片的概念應用到雜湊、集合和有序集合上,並在講解實現這些資料結構的其中一部分標準功能的方法。這種情況下,程式不再是將值 X
儲存到鍵 Y
裡面,而是將值 X
儲存到鍵 Y:<shardid>
裡面。 P214
對列表進行分片 P214
在不使用 Lua 指令碼的情況下對列表進行分配非常困難,因此將在後面介紹使用 Lua 指令碼構建一個分片式的列表,並支援以阻塞和非阻塞兩種方式從列表的兩端進行推入和彈出操作。
對有序集合進行分片 P215
因為 ZRANGE
, ZRANGEBYSCORE
, ZRANK
, ZCOUNT
, ZREMRANGE
, ZREMRANGEBYSCORE
這類命令的分片版本需要對有序集合的所有分片進行操作才能計算出命令的最終結果,所以這些操作無法執行得像普通的有序集合操作那麼快,因此對有序集合進行分片的作用不大。
如果需要將完整的資訊儲存到一個體積較大的有序集合中,但只會對分值排名前 n 位和後 n 位對元素進行操作,那麼可以使用下面介紹對雜湊分片對方法對有序集合進行分片,並維護額外對最高分值對有序集合和最低分值對有序集合,然後通過 ZADD
命令為這兩個有序集合新增新元素,並通過 ZREMRANGEBYRANK
命令確保元素對數量不會超過限制。 P215
分片式雜湊 P215
對雜湊的鍵進行劃分時,可以把雜湊儲存的鍵作為一個資訊源,並使用雜湊函式為鍵計算出一個數字雜湊值,然後根據需要儲存的鍵的總數量以及每個分片需要儲存的鍵數量,計算出所需的分片數,最後使用分片數和雜湊只決定應把鍵儲存到哪個分片裡面。 P215
所思
其實我們平時在考慮分片這種形式的時候是不太會考慮到鍵的總數量的這種條件,基本上是根據現有的資料進行分析後設定一個分片數量 shard_num
,這樣當有一個鍵 key
需要計算對應的分片時,只需要 cal_hash(key) % shard_num
即可得到對應的 shard_id
。但類似 CRC32
和 MD5
這種方式進行雜湊值時有一個問題,就是書中提到的當分片數量改變時,會有大量鍵的新舊雜湊值不同,就需要將資料遷移至新雜湊值對應的 shard_id
。為了避免這樣的情況,就需要一致性雜湊演算法,使得分片數量改變時需要遷移的資料儘量小一點,並保證遷移後的資料仍能夠較為均勻的在每個分片上。
將字串儲存到雜湊裡面 P217
如果發現將很多相關聯的短字串或者數字儲存到了字串鍵裡面,並且持續地將這些鍵命名為 namespace:id
這樣的形式,那麼可以考慮將這些值儲存到分片雜湊裡面,在某些情況下,這種做法可以明顯減少記憶體佔用。 P217
分片集合 P218
集合一樣可以通過類似雜湊的方式處理鍵獲得分片 id ,進而改造相應的命令支援分片式操作。
如果鍵是整數且最大值相對較小,那麼除了直接使用鍵獲取分片 id ,還可以使用點陣圖 (bitmap) 記錄每個鍵是否在“集合”中。 P221
如果鍵是整數,數量非常多,無法全部存下,但又能容忍一定的誤差,那麼可以使用布隆過濾器記錄每個鍵是否在“集合”中(判斷為不存在時,則必定不存在;判斷為存在時,有極低概率不存在)。
打包儲存二進位制位和位元組 P221
前面提到當使用類似 namespace:id
這樣當字串鍵去儲存短字串或者計數器時,使用分片雜湊可以有效降低儲存這些資料所需當記憶體。但是,如果被儲存的是一些簡短並且長度固定當連續 id ,那麼我們還有使用比分片雜湊更為節約記憶體當資料儲存方法可用。 P221
Redis 資料結構常用命令簡介 中介紹過可用於高效打包和更新 Redis 字串的四個命令: P221
GETRANGE
: 用於讀取被儲存字串的其中一部分內容SETRANGE
: 用於對儲存在字串裡面的其中一部分內容進行設定GETBIT
: 用於獲取字串裡面某個二進位制位的值SETBIT
: 用於對字串裡面某個二進位制位進行設定
通過這四個命令,我們可以在不對資料進行壓縮的情況下,使用 Redis 字串以儘可能緊湊的格式去儲存計數器、定長字串、布林值等資料。 P221
決定被儲存位置資訊的格式 P221
我們以儲存的資訊是使用者的位置資訊為例,不同記憶體的使用量決定了不同的位置精度: P221
- 1 位元組:精確到國家
- 2 位元組:精確到國家及所在州/省
- 3 位元組:精確到郵政編碼
- 4 位元組:精確到經緯度(2 米)
這裡我們用 2 位元組儲存位置資訊,首先我們可以使用一個陣列儲存所有國家(或地區)的 ISO3 國家(或地區)編碼,然後用第一個位元組儲存所在國家(或地區)在陣列中的下標。然後我們可以使用一個 map ,同樣使用陣列儲存每個國家(或地區)的州/省資訊,用第二個位元組儲存所在州/省在對應陣列中的下標。 P222
儲存打包後的資料 P223
獲取到位置資訊對應到兩個位元組到資料後,就可以使用 SETRANGE
命令將其儲存到字串鍵裡面去了。但是還需要考慮使用者的總量,假如使用者數量達到 7.5 億,那麼需要 1.5 GB 記憶體儲存所有使用者的資料,但 Redis 的字串鍵最大隻能儲存 512 MB 資料,並且 Redis 在對現有的字串進行設定的時候,如果被設定的部分超過了現有字串的末尾,那麼 Redis 可能需要分配更多的記憶體以儲存新資料,因此對一個長字串的末尾進行設定,耗時要比執行一個簡單的 SETBIT
呼叫多得多。為了解決上述問題,我們需要將資料分片到多個字串鍵裡面。 P223
我們可以在每個字串裡面儲存 2^20 個使用者的位置資訊,這相當於在字串裡面構建 100 多萬個節點,而這樣的字串需要佔 2 MB 的記憶體。 P223
對分片字串進行聚合計算 P224
對所有使用者的位置資訊進行聚合計算 P224
找到提前儲存的最大的使用者 id ,然後計算最大分片 id ,遍歷每個字串分片中的每個使用者的資料(使用 GETRANGE
分塊獲取資料),根據兩個位元組對應的下標找到對應的國家(或地區)及州/省資訊,然後統計即可。
對指定使用者的位置資訊進行聚合計算 P226
遍歷每個指定的使用者 id ,計算其對應的分片 id 和分片中的偏移量,使用 GETRANGE
獲取對應的兩個位元組,根據兩個位元組對應的下標找到對應的國家(或地區)及州/省資訊,然後統計即可。
本文首發於公眾號:滿賦諸機(點選檢視原文) 開源在 GitHub :reading-notes/redis-in-action