簡介
降低 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