Redis 已是你們耳熟能詳的東西了,平常工做也都在使用,面試中也是高頻的會涉及到,那麼咱們對它究竟瞭解有多深入呢?前端
我讀了幾本 Redis 相關的書籍,嘗試去了解它的具體實現,將一些底層的數據結構及實現原理記錄下來。面試
本文將介紹 Redis 中底層的 ziplist(壓縮列表) 的實現方法。 它是 Redis 中列表鍵和哈希鍵的底層實現之一。當符合某些狀況(後續文章講)時,列表鍵和哈希鍵會使用它。後端
能夠看到圖中,這個鍵值爲zsetkey
的 zset 內部使用的編碼方法就是 ziplist.數組
列表數據結構,咱們已經有了鏈表,爲何還須要從新搞一個壓縮列表呢?爲了節省內存。微信
鏈表的先後指針是一個很是耗費內存的結構,所以在數據量小的時候,這一部分的空間尤爲顯得浪費。數據結構
壓縮列表是一系列特殊編碼的連續內存塊組成的順序性數據結構。性能
這句話有點繞口,其實核心思想就是,在一塊連續的內存中,模擬出一個列表的結構。學習
壓縮列表的定義爲:編碼
struct ziplist<T>{
// 整個壓縮列表佔用字節數
int32 zlbytes;
// 最後一個節點到壓縮列表起始位置的偏移量,能夠用來快速的定位到壓縮列表中的最後一個元素
int32 zltail_offset;
// 壓縮列表包含的元素個數
int16 zllength;
// 元素內容列表,用數組存儲,內存上緊挨着
T[] entries;
// 壓縮列表的結束標誌位,值永遠爲 0xFF.
int8 zlend;
}
複製代碼
每一個字段的含義已經註釋在代碼中了。這裏額外解釋一下爲何須要 zltail_offset
這個屬性,由於壓縮列表只能順序遍歷,因此爲了提高效率,咱們須要能夠從首尾雙端來遍歷,用這個屬性能夠很快的找到壓縮列表的尾部。至於如何反向遍歷,請繼續向下看。url
壓縮列表的每個節點的定義爲:
struct entry{
// 前一個 entry 的長度
int<var> prevlous_entry_length;
// 編碼方式
int<vat> encoding;
// 內容
optional bute[] content;
}
複製代碼
定義裏,prevlous_entry_length 屬性,就是爲了反向遍歷而記錄的。想一下,首先拿到尾部節點的偏移量,找到最尾部的節點,而後調用prevlous_entry_length
屬性,就能夠拿到前一個節點,而後不斷向前遍歷了。
這裏須要注意的是:這個字段的長度並非必定的,它能夠是 1 個字節,也能夠是 5 個字節。
當前一個 entry 的長度在 254 字節之內的時候,這個屬性用一個字節來記錄。 不然就會用 5 個字節來記錄。
這回致使一個問題,見後文。
這個屬性記錄了節點的 content 屬性所保存的數據的類型以及長度。
這個屬性用來真正的保存節點的值,能夠是一個字節數組或者整數。它的類型和長度由 encoding 來決定。
在前言裏提到了,在某些狀況下列表鍵會使用壓縮列表,就是在列表鍵的內容比較少時,那麼壓縮列表爲何不能用於大的列表鍵呢?
ziplist 是連續存儲的數據結構,內存是沒有冗餘的(前面的文章講過的 SDS 中就有冗餘空間), 也就是說,每一次新增節點,都須要進行內存申請,而後將若是當前內存連續塊夠用,那麼將新節點添加,若是申請到的是另一塊連續內存空間,那麼須要將全部的內容拷貝到新的地址。
也就是說,每一次新增節點,都須要內存分配,可能還須要進行內存拷貝。當 ziplist 中存儲的值太多,內存拷貝將是一個很大的消耗。
也是所以,Redis 只在一些數據量小的場景下使用 ziplist.
在講prevlous_entry_length
的時候,咱們提到它的長度變化會致使一個問題,那就是級聯更新。
當前一個 entry 的長度在 254 字節之內的時候,這個屬性用一個字節來記錄。不然就會用 5 個字節來記錄。
那麼咱們設想一個極端的場景,在這個 ziplist 內部,全部的節點的長度都是 253 字節,也就意味着全部節點的prevlous_entry_length
屬性都是一個字節。
此時,咱們給壓縮列表最前端插入一個大於 254 字節的節點,那麼此時原來的第一個節點的prevlous_entry_length
屬性會從 1 個字節變成 5 個字節,這個節點的總長度也就來到了 257 字節,大於 254 字節,那麼下一個節點(原來的第二個節點)的prevlous_entry_length
屬性也會變成 5 個字節,這又會致使下一個節點的變化。... 引發連鎖變化,全部節點的prevlous_entry_length
值都須要更新一遍。
級聯更新的時間複雜度不好,最多須要進行 N 次空間的重分配,每次空間的重分配最差須要 O(N), 因此級聯更新的時間複雜度最差是 O(N2).
與新增節點類似,刪除節點也有可能會形成級聯更新的狀況。
可是其實不用怕,由於級聯更新形成 Redis 性能壓力的機率極其低。
首先,級聯更新須要連續的節點大小爲250-253
之間,這本就少見,而大範圍的連續就更加少見了。若是運氣很差出現了三五個的級聯更新,也毫不會對 Redis 的性能有壓力。
ziplist 是 Redis 單獨開發,用連續的內存空間來存儲 list 的一個數據結構。它的優點是沒有鏈表的先後指針的內存佔用,可是在數據量大的時候,性能有壓力。所以只用於數據量小的場景。
ziplist 是 list 鍵和 hash 鍵的底層實現數據結構之一。
ziplist 有一個問題,就是添加節點或者刪除節點,有極小的機率會觸發級聯更新,引發性能差別。可是這個事真的極小機率,不用擔憂。
《Redis 的設計與實現(第二版)》
《Redis 深度歷險:核心原理和應用實踐》
完。
最後,歡迎關注個人我的公衆號【 呼延十 】,會不按期更新不少後端工程師的學習筆記。 也歡迎直接公衆號私信或者郵箱聯繫我,必定知無不言,言無不盡。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客或關注微信公衆號 < 呼延十 >------>呼延十