redis數據結構之壓縮列表

壓縮列表用於存儲長度受限的字符串和整數。廢話不多說,直接上redis壓縮列表的內存結構示意圖:


從圖中可以看出,redis壓縮列表由表示壓縮列表佔總內存的字節數的zlbytes,表示到達ziplist 表尾節點的偏移量的zltail,表示ziplist 中節點的數量的zllen,各個節點以及用於標記ziplist的末端的zlend。

注意:zllen並不是一直表示節點的數量,只有zllen小於UINT16_MAX時纔是,當這個值等於UINT16_MAX時,節點的數量需要遍歷整個ziplist 才能計算得出。zlend的值是固定的,也就是255.

從圖中可以看出,壓縮列表總是有11個字節的固定長度(4+4+2+1),而這11個字節的長度也就是壓縮列表的頭部與尾部,在新建一個壓縮列表的時候,也就是隻有這11個字節,下面來看下新建壓縮列表的函數:

unsigned char *ziplistNew(void) {
    // 分配 2 個 32 bit,一個 16 bit,以及一個 8 bit
    // 分別用於 <zlbytes><zltail><zllen> 和 <zlend>
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
    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;
}
而ZIPLIST_HEADER_SIZE的定義是個宏,如下:
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t)) 
結合上面的宏可以看出ziplistNew中的bytes就是11。ziplistNew的功能也很簡單就不多說了。

在講述壓縮列表插入之前,先要介紹下其餘的東西。上面的圖講述了壓縮列表的格式,但是並沒有各個節點的格式,下面以一張圖來描述下:

上圖就描述了各個節點的格式,但是節點中域的長度並不是固定的,下面來講述下:

pre_entry_length從字面意思就能看出來它表示前一個節點的長度,pre_entry_length可以佔用1個字節也可以佔用5個字節。如果前一節點的長度小於254 字節,那麼只使用一個字節保存它的值。如果前一節點的長度大於等於254 字節,那麼將第1 個字節的值設爲254 ,然後用接下來的4 個字節保存實際長度。

encoding可以分爲四種,其中三種是爲字符串準備的,最後一種就是爲整數準備的。具體如下表格所示:


以上是編碼字符串所用到的。下面的是編碼整數的


下面結合插入數據到壓縮表來講述這些編碼方式。插入數據到壓縮表主要是通過ziplistInsert,這個函數會調用__ziplistInsert,而實際插入數據的也是__ziplistInsert這個函數來進行操作的。這裏我只列出比較重要的一部分代碼。

static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
.....
if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        // s 可以保存爲整數,那麼繼續計算保存它所需的空間
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipEncodeLength will use the
         * string length to figure out how to encode it. */
        // 不能保存爲整數,直接使用字符串長度
        reqlen = slen;
    }

    // 計算編碼 prevlen 所需的長度
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 計算編碼 slen 所需的長度
    reqlen += zipEncodeLength(NULL,encoding,slen);

// 如果添加的位置不是表尾,那麼必須確定後繼節點的 prevlen 空間
    // 足以保存新節點的編碼長度
    // zipPrevLenByteDiff 的返回值有三種可能:
    // 1)新舊兩個節點的編碼長度相等,返回 0
    // 2)新節點編碼長度 > 舊節點編碼長度,返回 5 - 1 = 4
    // 3)舊節點編碼長度 > 新編碼節點長度,返回 1 - 5 = -4
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

.....

// 如果新節點不是添加到列表末端,那麼它後面就有其他節點
    // 因此,我們需要移動這部分節點
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes */
        // 向右移動移原有數據,爲新節點讓出空間
        // O(N)
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        // 將本節點的長度編碼至下一節點
        zipPrevEncodeLength(p+reqlen,reqlen);

        /* Update offset for tail */
        // 更新 ziplist 的表尾偏移量
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);

        /* When the tail contains more than one entry, we need to take
         * "nextdiff" in account as well. Otherwise, a change in the
         * size of prevlen doesn't have an effect on the *tail* offset. */
        // 有需要的話,將 nextdiff 也加上到 zltail 上
        tail = zipEntry(p+reqlen);
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        /* This element will be the new tail. */
        // 更新 ziplist 的 zltail 屬性,現在新添加節點爲表尾節點
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

.....

if (nextdiff != 0) {
        offset = p-zl;
        // O(N^2)
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
......
}
代碼中大部分都有註釋,也比較簡單,這裏我要說的是if(nextdiff!=0)的情況,如果nextdiff不爲0,說明新插入的數據的長度與以前這個位置的數據長度不同,而next中的pre_entry_length就需要進行改變,所以要擴展或者收縮next的大小,next大小的改變也同時需要改變next下一個節點的pre_entry_length,直到整個壓縮列表全部進行更改。而上面的if語句就是做這件事的。


發佈了32 篇原創文章 · 獲贊 9 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章