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-壓縮列表 - 掘金


免責聲明!

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



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