redis:list的底层实现--压缩列表
壓縮列表是list和hash的底層實現之一。為了節約內存而開發的。
什么時候使用?
1)當list中的只包含少量列表項,每個列表項要么只包含小整數,要么就是長度比較短的字符串。
2)當hash里包含的kv都是小整數或者短字符串的話。
?
Redis壓縮列表原理與應用
壓縮列表是一種數據結構,這種數據結構的功能是將一系列數據與其編碼信息存儲在一塊連續的內存區域,這塊內存物理上是連續的,邏輯上被分為多個組成部分,其目的是在一定可控的時間復雜讀條件下盡可能的減少不必要的內存開銷,從而達到節省內存的效果,這么介紹有點玄乎,我們先一起看看它的實現原理吧,Redis3.2版本中,作者對壓縮列表的實現在ziplist.h和ziplist.c中。
壓縮列表原理????
我認為將數據按照一定規則存儲在內存中可以用“編碼”這個詞描述,因此下面會常用“編碼”這個詞。
總體編碼
上面說到壓縮列表是一塊連續的內存區域,這塊內存區域布編碼示意圖大致如下:
Redis壓縮列表內存編碼示意圖
常態的壓縮列表內存編碼如上圖所示,整個內存塊區域內分為五個部分,下面分別介紹著五個部分:
zlbytes:存儲一個無符號整數,固定四個字節長度,用于存儲壓縮列表所占用的字節,當重新分配內存的時候使用,不需要遍歷整個列表來計算內存大小。
zltail:存儲一個無符號整數,固定四個字節長度,代表指向列表尾部的偏移量,偏移量是指壓縮列表的起始位置到指定列表節點的起始位置的距離。
zllen:壓縮列表包含的節點個數,固定兩個字節長度,源碼中指出當節點個數大于2^16-2個數的時候,該值將無效,此時需要遍歷列表來計算列表節點的個數。
entryX:列表節點區域,長度不定,由列表節點緊挨著組成。每個節點可以保存一個字節數組或者是一個整數值!
zlend:一字節長度固定值為255,用于表示列表結束。
列表元素編碼
上面介紹了壓縮列表的總體內存布局,對于初entryX區域以外的四個區域的長度都是固定的,下面再看看entryX區域的編碼情況。
每個列表節點由三部分組成:
壓縮列表節點編碼示意圖
每個壓縮列表節點區域頭部包含兩部分,一部分叫做previous length,另一部分叫encoding,最后是主體內容,叫做content,下面分別介紹他們:
previous length
用于存儲上一個節點的長度,因此壓縮列表可以從尾部向頭部遍歷(利用 ztail 和privious_entry_length),即當前節點位置減去上一個節點的長度即得到上一個節點的起始位置。previous length的長度可能是1個字節或者是5個字節,如果上一個節點的長度小于254,則該節點只需要一個字節就可以表示前一個節點的長度了,如果前一個節點的長度大于等于254,則previous length的第一個字節為254,后面用四個字節表示當前節點前一個節點的長度。這么做很有效地減少了內存的浪費。
encoding
節點的encoding保存的是節點的content的內容類型以及長度,encoding類型一共有兩種,一種字節數組一種是整數,encoding區域長度為1字節、2字節或者5字節長。Redis作者巧妙的利用了前兩個字節來表示content存儲的內容類型和encoding區域的長度,我們先看看字節數組類型的encoding內容:
content為字節數組的encoding內容
再看看整數編碼類型的encoding內容:
content為整數的encoding內容
content
content區域用于保存節點的內容,節點內容類型和長度由encoding決定,上面可以看出目前content的內容類型有整數類型和字節數組類型,且某些條件下content的長度可能為0。
相信到這里,我們都明白了壓縮列表的原理,壓縮列表并不是對數據利用某種算法進行壓縮,而是將數據按照一定規則編碼在一塊連續的內存區域,目的是節省內存。下面我們看看壓縮列表在Redis中的應用領域。
Redis中壓縮列表的應用
Redis中,不同的數據類型廣泛地應用了壓縮列表編碼,整理如下表:
?
連鎖更新
每個節點的previous_entry_length屬性都記錄了前一個節點的長度
- 如果前一個節點的長度小于254,那么previous_entry_length屬性需要用1字節長的空間來保存這個長度值
- 如果前一個節點的長度大于等于254,那么previous_entry_length屬性需要5字節長的空間來保存這個長度值
考慮這樣一種情況:在一個壓縮列表中,有多個連續的、長度介于250字節到253字節之間的節點e1至eN
|zlbytes|zltail|zllen|e1|e2|e3|...|eN|zlend|
因為e1至eN的所有節點的長度都小于254字節,所以記錄這些節點的長度只需要1字節長的previous_entry_length屬性,換句話說,e1至eN的所有節點的previous_entry_length屬性都是1字節長的。
如果我們將一個長度大于等于254字節的新節點new設置到壓縮列表的表頭節點,那么new將成為e1的潛質節點。
此時e1到eN的每個節點的previous_entry_length屬性都要擴展為5字節以符合壓縮列表對節點的要求,程序需要不斷的對壓縮列表進行空間重分配操作。
Redis將這種在特殊情況下產生的多次空間擴展操作稱之為“連鎖更新”。
除了添加新節點可能會引發連鎖更新之外,刪除節點也可能會連鎖更新。
因為連鎖更新在最壞情況下需要對壓縮列表執行N次空間重分配操作,而每次空間重分配的最壞復雜度為O(N),所以連鎖更新的最壞復雜度為O(N2)。
注意的是,盡管連鎖更新的復雜度較高,但它真正趙成性能問題的幾率是很低的: - 首先,壓縮列表里要恰好有多個連續的、長度介于250字節至253字節之間的節點,連鎖更新才有可能被引發,在實際中,這種情況并不多見;
- 其次,即使出現連鎖更新,但只要更新的節點數量不多,就不會對性能造成任何影響:比如說,對三五個節點進行連鎖更新是絕對不會影響性能的;
?
Redis中數據結構類型與壓縮列表的應用
上表總結了壓縮列表編碼在Redis不同的數據類型中的應用,Redis一共支持五種數據結構類型,其中有三種數據結構在一定條件下會應用壓縮列表,至于什么條件后面會分析,值得一提的是Redis當前支持的GEO(地理位置)對壓縮列表也有應用,具體此處不做討論。
Redis壓縮列表應用分析
上面部分介紹了Redis壓縮列表的原理與應用,下面簡單分析一下,主要從通過試圖回答一些問題來分析:Redis為什么使用壓縮列表?使用壓縮列表的好處是什么?使用壓縮列表的好處還有什么?壓縮列表的應用對與我們使用內存有沒有什么啟發?
Redis對于每種數據結構、無論是列表、哈希表還是有序集合,在決定是否應用壓縮列表作為當前數據結構類型的底層編碼的時候都會依賴一個開關和一個閾值,開關用來決定我們是否要啟用壓縮列表編碼,閾值總的來說通常指當前結構存儲的key數量有沒有達到一個數值(條件),或者是value值長度有沒有達到一定的長度(條件)。任何策略都有其應用場景,不同場景應用不同策略。為什么當前結構存儲的數據條目達到一定數值使用壓縮列表就不好?壓縮列表的新增、刪除的操作平均時間復雜度為O(N),隨著N的增大,時間必然會增加,他不像哈希表可以以O(1)的時間復雜度找到存取位置,然而在一定N內的時間復雜度我們可以容忍。然而壓縮列表利用巧妙的編碼技術除了存儲內容盡可能的減少不必要的內存開銷,將數據存儲于連續的內存區域,這對于Redis本身來說是有意義的,因為Redis是一款內存數據庫軟件,想辦法盡可能減少內存的開銷是Redis設計者一定要考慮的事情。
另外,經過仔細琢磨,我認為使用壓縮列表的好處除了節約內存之外,還有減少內存碎片的作用,我把這種行為叫做"合并存儲",也就是將很多小的數據塊存儲在一個比較大的內存區域,試想想,如果我們將要存儲的數據都是很小的條目,我們為每一個數據條目都單獨的申請內存,結果是這些條目將有可能分散在內存的每一個角落,最終導致碎片增加,這是一件令人頭疼的事情。
總結
這篇文章在介紹Redis壓縮列表原理與應用的基礎之上對Redis壓縮列表的應用進行分析,分析部分主要摻雜著個人的理解與認知,如果有不同觀點或者補充觀點,歡迎留言討論。
?
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的redis:list的底层实现--压缩列表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 框架:Spring事务的隔离级别
- 下一篇: Proactor设计模式