1. 程式人生 > >Redis面試熱點之底層實現篇(續)

Redis面試熱點之底層實現篇(續)

0.題外話

接著昨天的【決戰西二旗】|Redis面試熱點之底層實現篇繼續來了解一下ziplist壓縮列表這個資料結構。

你可能會抱有疑問:我只是使用Redis的功能並且公司的運維同事都已經搭建好了平臺,只需要線上申請一下配置和獲取連線的地址就可以愉快地使用了,為啥還要這麼深入的理解底層的資料結構呢?有啥用呢?

其實這個問題可以分幾個方面去回答吧,筆者試著去解釋一下原因:

  1. 好奇心 作為技術人員,沒有好奇心會讓我們錯過很多精彩,難道你對如此強悍的NoSQL是如何跑起來的不感興趣嗎?好奇心讓我們知道的更多,也讓我們不知道的更多;
  2. 辯證的職業素養 無論是996還是快速迭代,讓很多人陷入了網上找找、github找找、改改、跑起來萬事大吉的迴圈,但是這種確實是溫水煮青蛙,長此以往我們將漸漸失去判斷力,再者國內的文章或者技術點說明基本上都是抄來抄去,沒有好的鑑別能力往往就走很多彎路,在這個過程中職業素養就顯得意義重大,算是比較核心的競爭力了;
  3. 善於思考的習慣 個人認為了解原始碼的一個好處是能對其中的原理有一定認識,不至於完全黑盒子一樣使用,調包Boy或者API工程師往往在遇到一些問題是束手無策,因為不知道是什麼原因造成的。更重要的一個好處在於對原始碼的學習本質上是相同問題的遷移,換句話說,我們有時候抱怨自己接觸不到高難度的專案,無法快速提高自己的能力,但是個人覺得如果能力不夠給你高難度專案只能讓你失眠,因為當沒有可借鑑可參考的過往專案時會讓你束手無策,因為從0-1做一個好專案的能力不是一天養成的。深入研究開源工程的實現細節能讓我們置身相同的境況來思考問題,假如自己被指定去完成該任務,那麼要如何實現呢?
  4. 胸有成竹的能力 我們有個成語叫庖丁解牛,就是說我們掌握了事物的客觀規律,就能運用自如。經驗豐富的人在拿到一個任務之後,腦海裡便可以浮現出這個任務需要被拆解成幾部分,設計的重難點是什麼,其中可能出現的坑是什麼,需要使用哪些方法來解決特殊的問題,個人感覺閱讀原始碼可以讓你慢慢獲得這種能力,試想你和大師面對一樣的問題,你先深入思考如何去做,然後再仔細研究大師的方案,久而久之自己的功力也必然會提升,我覺得這也是研究開源工程原始碼最重要的原因。

說了這麼多,無非是想表達,帶著思考去學習,受益的必然是你自己,大的方針政策是正確的,剩下的就是一步步去執行了,原始碼工程千千萬,那也不必著急,核心的思想並沒有那麼多,怕什麼真理無窮,進一寸有一寸的歡喜!

 

Q6:Redis的ziplist是如何實現的?壓縮列表的連鎖更新的原因瞭解嗎?
前面的文章介紹了zset和hash在資料量少且長度滿足一定條件的基礎上就會選擇使用ziplist來進行儲存。

當然後面antirez又推出了quicklist的結構,後續可以聊聊quicklist,不過快速連結串列也是基於壓縮列表實現的,ziplist是一種使用特殊編碼的記憶體連續型的資料結構,讓我們來一起揭開ziplist的神祕面紗吧。

1.如何設計ziplist

先不看Redis的對ziplist的具體實現,我們先來想一下如果我們來設計這個資料結構需要做哪些方面的考慮呢?思考式地學習收穫更大呦!

  • 考慮點1:連續記憶體的雙面性
    連續型記憶體減少了記憶體碎片,但是連續大記憶體又不容易滿足。
    這個非常好理解,你和好基友三人去做地鐵,你們三個挨著坐肯定不浪費空間,但是地鐵裡很多人都是單獨出行的,大家都不願意緊挨著,就這樣有2個的位置有1個的位置,可是3個連續的確實不好找呀,來張圖:

  • 考慮點2: 壓縮列表承載元素的多樣性
    待設計結構和陣列不一樣,陣列是已經強制約定了型別,所以我們可以根據元素型別和個數來確定索引的偏移量,但是壓縮列表對元素的型別沒有約束,也就是說不知道是什麼資料型別和長度,這個有點像TCP粘包拆包的做法了,需要我們指定結尾符或者指定單個儲存的元素的長度,要不然資料都粘在一起了。
  • 考慮點3:屬性的常數級耗時獲取
    就是說我們解決了前面兩點考慮,但是作為一個整體,壓縮列表需要常數級消耗提供一些總體資訊,比如總長度、已儲存元素數量、尾節點位置(實現尾部的快速插入和刪除)等,這樣對於操作壓縮列表意義很大。
  • 考慮點4:資料結構對增刪的支援
    理論上我們設計的資料結構要很好地支援增刪操作,當然凡事必有權衡,沒有什麼資料結構是完美的,我們邊設計邊調整吧。
  • 考慮點5:如何節約記憶體
    我們要節約記憶體就需要特殊情況特殊處理,所謂變長設計,也就是不像雙向連結串列一樣固定使用兩個pre和next指標來實現,這樣空間消耗更大,因此可能需要使用變長編碼。

2.ziplist總體結構

大概想了這麼多,我們來看看Redis是如何考慮的,筆者又畫了一張總覽簡圖:

從圖中我們基本上可以看到幾個主要部分:zlbytes、zltail、zllen、zlentry、zlend。
來解釋一下各個屬性的含義,借鑑網上一張非常好的圖,其中紅線驗證了我們的考慮點2、綠線驗證了我們的考慮點3:

來看下ziplist.c中對ziplist的申請和擴容操作,加深對上面幾個屬性的理解:

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}

/* Resize the ziplist. */
unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
    zl = zrealloc(zl,len);
    ZIPLIST_BYTES(zl) = intrev32ifbe(len);
    zl[len-1] = ZIP_END;
    return zl;
}

3.zlentry的實現

  • encoding編碼和content儲存

我們再來看看zlentry的實現,encoding的具體內容取決於content的型別和長度,其中當content是字串時encoding的首位元組的高2bit表示字串型別,當content是整數時,encoding的首位元組高2bit固定為11,從Redis原始碼的註釋中可以看的比較清楚,筆者再做一層漢語版的註釋^_^:

/*
 ###########字串儲存詳解###############
 #### encoding部分分為三種類型:1位元組、2位元組、5位元組 ####
 #### 最高2bit表示是哪種長度的字串 分別是00 01 10 各自對應1位元組 2位元組 5位元組 ####

 #### 當最高2bit=00時 表示encoding=1位元組 剩餘6bit 2^6=64 可表示範圍0~63####
 #### 當最高2bit=01時 表示encoding=2位元組 剩餘14bit 2^14=16384 可表示範圍0~16383####
 #### 當最高2bit=11時 表示encoding=5位元組 比較特殊 用後4位元組 剩餘32bit 2^32=42億多####
 * |00pppppp| - 1 byte
 *      String value with length less than or equal to 63 bytes (6 bits).
 *      "pppppp" represents the unsigned 6 bit length.
 * |01pppppp|qqqqqqqq| - 2 bytes
 *      String value with length less than or equal to 16383 bytes (14 bits).
 *      IMPORTANT: The 14 bit number is stored in big endian.
 * |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
 *      String value with length greater than or equal to 16384 bytes.
 *      Only the 4 bytes following the first byte represents the length
 *      up to 32^2-1. The 6 lower bits of the first byte are not used and
 *      are set to zero.
 *      IMPORTANT: The 32 bit number is stored in big endian.

 *########################字串儲存和整數儲存的分界線####################*
 *#### 高2bit固定為11 其後2bit 分別為00 01 10 11 表示儲存的整數型別
 * |11000000| - 3 bytes
 *      Integer encoded as int16_t (2 bytes).
 * |11010000| - 5 bytes
 *      Integer encoded as int32_t (4 bytes).
 * |11100000| - 9 bytes
 *      Integer encoded as int64_t (8 bytes).
 * |11110000| - 4 bytes
 *      Integer encoded as 24 bit signed (3 bytes).
 * |11111110| - 2 bytes
 *      Integer encoded as 8 bit signed (1 byte).
 * |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
 *      Unsigned integer from 0 to 12. The encoded value is actually from
 *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
 *      subtracted from the encoded 4 bit value to obtain the right value.
 * |11111111| - End of ziplist special entry.
*/

content儲存節點內容,其內容可以是位元組陣列和各種型別的整數,它的型別和長度決定了encoding的編碼,對照上面的註釋來看兩個例子吧:

儲存位元組陣列:編碼的最高兩位00表示節點儲存的是一個位元組陣列,編碼的後六位001011記錄了位元組陣列的長度11,content 屬性儲存著節點的值 "hello world"。

儲存整數:編碼為11000000表示節點儲存的是一個int16_t型別的整數值,content屬性儲存著節點的值10086。

  • prevlen屬性

最後來說一下prevlen這個屬性,該屬性也比較關鍵,前面一直在說壓縮列表是為了節約記憶體設計的,然而prevlen屬性就恰好起到了這個作用,回想一下連結串列要想獲取前面的節點需要使用指標實現,壓縮列表由於元素的多樣性也無法像陣列一樣來實現,所以使用prevlen屬性記錄前一個節點的大小來進行指向。

prevlen屬性以位元組為單位,記錄了壓縮列表中前一個節點的長度,其長度可以是 1 位元組或者 5 位元組:

  1. 如果前一節點的長度小於254位元組,那麼prevlen屬性的長度為1位元組, 前一節點的長度就儲存在這一個位元組裡面。
  2. 如果前一節點的長度大於等於254位元組,那麼prevlen屬性的長度為5位元組,第一位元組會被設定為0xFE,之後的四個位元組則用於儲存前一節點的長度。

思考:注意一下這裡的第一位元組設定的是0xFE而不是0xFF,想下這是為什麼呢?
沒錯!前面提到了zlend是個特殊值設定為0xFF表示壓縮列表的結束,因此這裡不可以設定為0xFF,關於這個問題在redis有個issue,有人提出來antirez的ziplist中的註釋寫的不對,最終antirez發現註釋寫錯了,然後愉快地修改了,哈哈!

再思考一個問題,為什麼prevlen的長度要麼是1位元組要麼是5位元組呢?為啥沒有2位元組、3位元組、4位元組這些中間態的長度呢?要解答這個問題就引出了今天的一個關鍵問題:連鎖更新問題。

4.連鎖更新問題

試想這樣一種增加節點的場景:

如果在壓縮列表的頭部增加一個新節點,並且長度大於254位元組,所以其後面節點的prevlen必須是5位元組,然而在增加新節點之前其prevlen是1位元組,必須進行擴充套件,極端情況下如果一直都需要擴充套件那麼將產生連鎖反應:

試想另外一種刪除節點的場景:

如果需要刪除的節點時小節點,該節點前面的節點是大節點,這樣當把小節點刪除時,其後面的節點就要保持其前面大節點的長度,面臨著擴充套件的問題:

理解了連鎖更新問題,再來看看為什麼要麼1位元組要麼5位元組的問題吧,如果是2-4位元組那麼可能產生連鎖反應的概率就更大了,相反直接給到最大5位元組會大大降低連鎖更新的概率,所以筆者也認為這種記憶體的小小浪費也是值得的。

5.辯證看待ziplist

從ziplist的設計來看,壓縮列表並不擅長修改操作,這樣會導致記憶體拷貝問題,並且當壓縮列表儲存的資料量超過某個閾值之後查詢指定元素帶來的遍歷損耗也會增加。

6.巨人的肩膀

redis-壓縮列表 - 掘金

http://zhangtielei.com/posts/blog-redis-ziplist.html

https://github.com/antirez/redis/blob/unstable/src/zipli