作者:Xie Zefan
來源:https://xiezefan.me/2017/05/01/redis_in_action_ziplist/
在討論Redis記憶體壓縮的時候,我們需要了解一下幾個Redis的相關知識,
壓縮串列 ziplist
Redis的ziplist是用一段連續的記憶體來存盤串列資料的一個資料結構,它的結構示例如下圖
壓縮串列組成示例--截圖來自《Redis設計與實作》
- zlbytes: 記錄整個壓縮串列使用的記憶體大小
- zltail: 記錄壓縮串列表尾距離起始位置有多少位元組
- zllen: 記錄壓縮串列節點數量,值得注意的一點是,因為它只占了2個位元組,所以最大值只能到65535,這意味著壓縮串列長度大于65535的時候,就只能通過遍歷整個串列來計算長度了
- zleng: 壓縮串列末端標志位,固定值為
OxFF - entry1-N: 壓縮串列節點, 具體結構如下圖
壓縮串列節點組成示例--截圖來自《Redis設計與實作》
其中
- previous_entry_length: 上一個節點的長度
- encoding: content的編碼以及長度
- content: 節點資料
當我們查找一個節點的時候,主要進行一下操作:
- 根據zltail獲取最后一個節點的位置
- 判斷當前節點是否是目標節點
- 如果是,則回傳資料
- 如果不是,則根據previous_entry_length計算上一個節點的起始位置,然后重新進行步驟2判斷
通過上述的描述,我們可以知道,ziplist每次資料更新的復雜度大約是O(N),因為它需要對N個節點進行記憶體重分配,查找一個資料的時候,復雜度是O(N),最壞情況下需要遍歷整個串列,
什么情況下會使用到ziplist呢?
Redis會使用到ziplist的資料結構是Hash與List,
Hash結構使用ziplist作為底層存盤的兩個條件是:
- 所有的鍵與值的字串長度都小于64位元組的時候
- 鍵與值對資料小于512個
只要上述條件任何一個不滿足,Redis就會自動將這個Hash物件從ziplist轉換成hashtable,但這兩個閾值可以通過修改組態檔中的hash-max-ziplist-value與hash-max-ziplist-entries來變更,
List結構使用ziplist的條件與Hash結構一樣,當條件不滿足的時候,會從ziplist轉換成linkedlist,同樣我們可以修改list-max-ziplist-value與hash-max-ziplist-entries來使用不同的閾值,
為什么Hash與List會使用ziplist來存盤資料呢?
因為
- ziplist會比hashtable與ziplist節省跟多的記憶體
- 記憶體中以連續塊方式保存的資料比起hashtable與linkedlist使用的鏈表可以更快的載入快取中
- 當ziplist的長度比較小的時候,從ziplist讀寫資料的效率比hashtable或者linkedlist的差異并不大,
本質上,使用ziplist就是以時間換空間的一種優化,但是他的時間損壞小到幾乎可以忽略不計,但卻能帶來可觀的記憶體減少,所以滿足條件時,Redis會使用ziplist作為Hash與List的存盤結構,
實戰
我們先拋出問題,在廣告程式化交易的程序中,我們經常需要為一個廣告投放計劃定制人群包,其存盤的形式如下:
人群包ID => [設備ID_1, 設備ID_2 ... 設備ID_N]
其中,人群包ID是Long型整數,設備ID是經過MD5處理,長度為32,
在業務場景中,我們需要判斷一個設備ID是否在一個人群包中,來決定是否投放廣告,
在傳統的使用Redis的場景, 我們可以使用標準的KV結構來存盤定向包資料,則存盤方式如下:
{人群包ID}_{設備ID_1} => true
{人群包ID}_{設備ID_2} => true
如果我們想使用ziplist來繼續記憶體壓縮的話,我們必須保證Hash物件的長度小于512,并且鍵值的長度小于64位元組,我們可以將KV結構的資料,存盤到預先分配好的bucket中,
我們先預估下,整個Redis集群預計容納的資料條數為10億,那么Bucket的數量的計算公式如下:
bucket_count = 10億 / 512 = 195W
那么我們大概需要200W個Bucket(預估Bucket數量需要多預估一點,以防觸發臨界值問題)
我們先以下公式計算BucketID:
bucket_id = CRC32(人群包ID + "_" + 設備ID) % 200W
那么資料在Redis的存盤結構就變成
bucket_id => {
{人群包ID}_{設備ID_1} => true
{人群包ID}_{設備ID_2} => true
}
這樣我們保證每個bucket中的資料項都小于512,并且長度均小于64位元組,
我們以2000W資料進行測驗,前后兩者的記憶體使用情況如下:
| 資料集大小 | 存盤模式 | Bucket數量 | 所用記憶體 | 碎片率 | Redis占用的記憶體 |
|---|---|---|---|---|---|
| 2000W | 壓縮串列 | 200W | 928M | 1.38 | 1.25G |
| 2000W | 壓縮串列 | 5W | 785M | 1.48 | 1.14G |
| 2000W | 直接存盤 | - | 1.44G | 1.03 | 1.48G |
在這里需要額外引入一個概念 – 記憶體碎片率,
記憶體碎片率 = 作業系統給Redis分配的記憶體 / Redis存盤物件占用的記憶體
因為壓縮串列在更新節點的時候,經常需要進行記憶體重分配,所以導致比較高的記憶體碎片率,我們在做技術方案比較的時候,記憶體碎片率也是非常需要關注的指標之一,
但有很多手段可以減少記憶體碎片率,比如記憶體對其,甚至更極端的直接重做整個Redis記憶體(利用快斬訓者從節點來重做記憶體)都能有效的減低記憶體碎片率,
我們在本次實驗中,因為存盤的數值比較大(單個KEY約34個位元組),所以實際節省記憶體不是很多,但依然能節約35%-50%的記憶體使用,
在實際的生產環境中,我們根據應用場景合理的設計壓縮存盤結構,部分業務甚至能達到節約70%的記憶體使用的效果,
壓縮串列能節省多少記憶體?
我們現在知道壓縮串列是通過將節點緊湊的排列在記憶體中,從而節省掉記憶體的,但他究竟節省了哪些記憶體從而能達到驚人的壓縮率呢?
首先為了明白這個細節,我們需要知道普通Key-Value結構在Redis中是如何存盤的,
typedef struct redisObject {
unsigned type:4; // 物件的型別
unsigned encoding:4; // 物件的編碼
unsigned lru:LRU_BITS; // LRU型別
int refcount; // 參考計數
void *ptr; // 指向底層資料結構的指標
} robj;
Redis所有的物件都是通過上述結構來存盤, 假設我存盤Hello=>World這樣一個健值對到Redis中,除了存盤本身鍵值的資料外,還需要額外的24個位元組來存盤redisObject物件,
而Redis存盤字串使用的SDS資料結構
struct sdshdr8 {
uint8_t len; // 所保存字串的長度
uint8_t alloc; // 分配的記憶體數量
unsigned char flags;// 標志位,用于判斷sdshdr型別
char buf[]; // 位元組陣列,用戶保存字串
};
假如字串的長度無法用unsigned int8來表示的話,Redis會使用能表達更大長度的sdshdr16結構來存盤字串,
并且,為了減少修改字串帶來的記憶體重分類問題,Redis會進行記憶體預分配,所以可能你僅僅為了保存五個字符,但Redis會為你預分配10 bytes的記憶體,
這意味著當我們存盤Hello這個字串的時候,你需要額外的3個以上的位元組,
Oh~~~,我只想保存Hello=>World這十個字符的資料,竟然需要的30~40個位元組的資料來存盤額外的資訊,比存盤資料本身的大小還多一些,這還沒包括Redis維護字典表所需要的額外的記憶體空間,
那么假設我們用ziplist來存盤這個資料,我們僅僅需要額外的2個位元組用于存盤previous_entry_length與encoding,具體的計算方式可以參考Redis原始碼或者《Redis設計與實作》第一部分第7章壓縮串列,
總結
從以上對比,我們可以看出,在存盤越小的資料的時候,使用ziplist來進行資料壓縮能得到更好的壓縮率,
但副作用也很明顯,ziplist的更新效率遠遠低于普通K-V模式,并且會造成額外的記憶體碎片率,
在Redis中存盤大量資料的實踐程序中,我們經常會做一些小技巧來盡可能壓榨Redis的存盤能力,接下來準備寫一篇Redis記憶體壓縮的小技巧,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.終于靠開源專案弄到 IntelliJ IDEA 激活碼了,真香!
3.阿里 Mock 工具正式開源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式發布,全新顛覆性版本!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/289033.html
標籤:其他
上一篇:37.qt quick- 高仿微信實作局域網聊天V3版本(添加登錄界面、UDP校驗登錄、皮膚更換、3D旋轉)
下一篇:Leetcode No.122 Best Time to Buy and Sell Stock II Easy(c++實作)
