Redis 實戰 —— 12. 降低內存占用


簡介

降低 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 。但類似 CRC32MD5 這種方式進行散列值時有一個問題,就是書中提到的當分片數量改變時,會有大量鍵的新舊散列值不同,就需要將數據遷移至新散列值對應的 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM