上一篇咱們介紹了 redis 中的整數集合這種數據結構的實現,也談到了,引入這種數據結構的一個很大的緣由就是,在某些僅有少許整數元素的集合場景,經過整數集合既能夠達到字典的效率,也能使用遠少於字典的內存達到一樣的效果。java
咱們本篇介紹的壓縮列表,相信你從他的名字裏應該也能看出來,又是一個爲了節約內存而設計的數據結構,它的數據結構相對於整數集合來講會複雜了不少,可是整數集合只能容許存儲少許的整型數據,而咱們的壓縮列表能夠容許存儲少許的整型數據或字符串。git
這是他們之間的一個區別,下面咱們來看看這種數據結構。程序員
其中,zlentry 在 redis 中確實有着這樣的結構體定義,但實際上這個結構定義了一堆相似於 length 這樣的字段,記錄前一個節點和自身節點佔用的字節數等等信息,用處很少,而咱們更傾向於使用這樣的邏輯結構來描述 zlentry 節點。github
這種結構在 redis 中是沒有具體結構體定義的,請知悉,網上的不少博客文章都直接描述 zlentry 節點是這樣的一種結構,實際上是不許確的。redis
簡單解釋一下這三個字段的含義:數組
下面咱們細說一個 encoding 具體是怎麼存儲的。bash
主要分爲兩種,一種是字符串的存儲格式:微信
編碼 | 編碼長度 | content類型 |
---|---|---|
00xxxxxx | 一個字節 | 長度小於 63 的字符串 |
01xxxxxx xxxxxxxx | 兩個字節 | 長度小於 16383 的字符串 |
10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx | 五個字節 | 長度小於 4294967295 的字符串 |
content 的具體長度,由編碼除去高兩位剩餘的二進制位表示。markdown
編碼 | 編碼長度 | content類型 |
---|---|---|
11000000 | 一個字節 | int16_t 類型的整數 |
11010000 | 一個字節 | int32_t 類型的整數 |
11100000 | 一個字節 | int64_t 類型的整數 |
11110000 | 一個字節 | 24 位有符號整數 |
11111110 | 一個字節 | 8 位有符號整數 |
注意,整型數據的編碼是固定 11 開頭的八位二進制,而字符串類型的編碼都是非固定的,由於它還須要經過後面的二進制位獲得字符串的長度,稍有區別。數據結構
這就是壓縮列表的基本的結構定義狀況,下面咱們經過節點的增刪改查方法源碼實現來看看 redis 中具體的實現狀況。
一、ziplistNew
咱們先來看看壓縮列表初始化的方法實現:
unsigned char *ziplistNew(void) { //bytes=2*4+2 //分配壓縮列表結構所須要的字節數 //ZIPLIST_BYTES + ZIPLIST_TAIL_OFFSET + ZIPLIST_LENGTH unsigned int bytes = ZIPLIST_HEADER_SIZE+1; unsigned char *zl = zmalloc(bytes); //初始化 ZIPLIST_BYTES 字段 ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); //初始化 ZIPLIST_TAIL_OFFSET ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); //初始化 ZIPLIST_LENGTH 字段 ZIPLIST_LENGTH(zl) = 0; //爲壓縮列表最後一個字節賦值 255 zl[bytes-1] = ZIP_END; return zl; } 複製代碼
二、ziplistPush
接着咱們看新增節點的源碼實現:
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s ,unsigned int slen, int where) { unsigned char *p; //找到待插入的位置,頭部或者尾部 p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl); return __ziplistInsert(zl,p,s,slen); } 複製代碼
解釋一下 ziplistPush 的幾個入參的含義。
zl 指向一個壓縮列表的首地址,s 指向一個字符串首地址),slen 指向字符串的長度(若是節點存儲的值是整型,存儲的就是整型值),where 指明新節點的插入方式,頭插亦或尾插。
ziplistPush 方法的核心是 __ziplistInsert:
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; unsigned int prevlensize, prevlen = 0; size_t offset; int nextdiff = 0; unsigned char encoding = 0; long long value = 123456789; zlentry tail; //prevlensize 存儲前一個節點長度,本節點使用了幾個字節 1 or 5 //prelen 存儲前一個節點實際佔用了幾個字節 if (p[0] != ZIP_END) { ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLength(ptail); } } if (zipTryEncoding(s,slen,&value,&encoding)) { //s 指針指向一個整數,嘗試進行一個轉換並獲得存儲這個整數佔用了幾個字節 reqlen = zipIntSize(encoding); } else { //s 指針指向一個字符串(字符數組),slen 就是他佔用的字節數 reqlen = slen; } //當前節點存儲數據佔用 reqlen 個字節,加上存儲前一個節點長度佔用的字節數 reqlen += zipStorePrevEntryLength(NULL,prevlen); //encoding 字段存儲實際佔用字節數 reqlen += zipStoreEntryEncoding(NULL,encoding,slen); //至此,reqlen 保存了存儲當前節點數據佔用字節數和 encoding 編碼佔用的字節數總和 int forcelarge = 0; //當前節點佔用的總字節減去存儲前一個節點字段佔用的字節 //記錄的是這一個節點的插入會引發下一個節點佔用字節的變化量 nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; if (nextdiff == -4 && reqlen < 4) { nextdiff = 0; forcelarge = 1; } //擴容有可能致使 zl 的起始位置偏移,故記錄 p 與 zl 首地址的相對誤差數,過後還原 p 指針指向 offset = p-zl; zl = ziplistResize(zl,curlen+reqlen+nextdiff); p = zl+offset; if (p[0] != ZIP_END) { memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); //把當前節點佔用的字節數存儲到下一個節點的頭部字段 if (forcelarge) zipStorePrevEntryLengthLarge(p+reqlen,reqlen); else zipStorePrevEntryLength(p+reqlen,reqlen); //更新 tail_offset 字段,讓他保存從頭節點到尾節點之間的距離 ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); zipEntry(p+reqlen, &tail); if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff); } } else { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl); } //是否觸發連鎖更新 if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); p = zl+offset; } //將節點寫入指定位置 p += zipStorePrevEntryLength(p,prevlen); p += zipStoreEntryEncoding(p,encoding,slen); if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); } else { zipSaveInteger(p,value,encoding); } ZIPLIST_INCR_LENGTH(zl,1); return zl; } 複製代碼
具體細節我再也不贅述,總結一下整個插入節點的步驟。
ps:重點要去理解壓縮列表節點的數據結構定義,previous_entry_length、encoding、content 字段,這樣才能比較容易理解節點新增操做的實現。
談到 redis 的壓縮列表,就必然會談到他的連鎖更新,咱們先引一張圖:
假設本來 entry1 節點佔用字節數爲 211(小於 254),那麼 entry2 的 previous_entry_length 會使用一個字節存儲 211,如今咱們新插入一個節點 NEWEntry,這個節點比較大,佔用了 512 個字節。
那麼,咱們知道,NEWEntry 節點插入後,entry2 的 previous_entry_length 存儲不了 512,那麼 redis 就會重分配內存,增長 entry2 的內存分配,並分配給 previous_entry_length 五個字節存儲 NEWEntry 節點長度。
看似沒什麼問題,可是若是極端狀況下,entry2 擴容四個字節後,致使自身佔用字節數超過 254,就會又觸發後一個節點的內存佔用空間擴大,很是極端狀況下,會致使全部的節點都擴容,這就是連鎖更新,一次更新致使大量甚至所有節點都更新內存的分配。
若是連鎖更新發生的機率很高的話,壓縮列表無疑就會是一個低效的數據結構,但實際上連鎖更新發生的條件是很是苛刻的,其一是須要大量節點長度小於 254 連續串聯鏈接,其二是咱們更新的節點位置剛好也致使後一個節點內存擴充更新。
基於這兩點,且少許的連鎖更新對性能是影響不大的,因此這裏的連鎖更新對壓縮列表的性能是沒有多大的影響的,能夠忽略,但須要知曉。
一樣的,若是以爲我寫的對你有點幫助的話,順手點一波關注吧,也歡迎加做者微信深刻探討,咱們逐漸開始走近 redis 比較實用性的相關內容了,盡請關注。
關注公衆不迷路,一個愛分享的程序員。 公衆號回覆「1024」加做者微信一塊兒探討學習! 每篇文章用到的全部案例代碼素材都會上傳我我的 github github.com/SingleYam/o… 歡迎來踩!