Redis學習筆記(二)redis 底層資料結構

Ethan_Wong發表於2022-02-05

在上一節提到的圖中,我們知道,可以通過 redisObject 物件的 type 和 encoding 屬性。可以決定Redis 主要的底層資料結構:SDS、QuickList、ZipList、HashTable、IntSet、ZskipList 。

一、簡單動態字串(SDS)

先來看看傳統的C 語言如何儲存字串的:比如一個 "Redis" 字串:

為什麼不用傳統的 C 語言的方式,因為我們知道陣列方式在獲取字串長度或者擴容上存在缺陷:比如獲得一個陣列長度的複雜度為O(N), 而且陣列擴容也不太方便。所以自己構建了一種名為簡單動態字串(simple dynamic String)的抽象資料型別。

  • sdshdr : 表示SDS 型別,共有5種型別。每個型別的數字表示 unit
  • alloc:還未被使用的空間, 這裡為0
  • len:表示這個 SDS 儲存了5 個單位的字串
  • buf:char型別的陣列,用於儲存字元

1.1 SDS 的定義

SDS 位於 src/sds.h 和 src/sds.c 中,它的結構總共有五種(redis 6.0.6)

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    //所儲存字串的長度
    uint8_t len; /* used */
    //除了頭部與末尾的\0以外,剩餘的位元組數
    uint8_t alloc; /* excluding the header and null terminator */
    //只有1位元組,前5位未使用,後三位表示頭部的型別(sdshdr5\8\16\32\64)
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    //用來儲存字串的元素
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

1.2 SDS 和 C 字串的區別

  • SDS 能以O(1) 複雜度獲取字串長度:SDS 有 len 屬性,而 C 字串沒有

  • SDS 能杜絕緩衝區溢位

    • 快取區溢位:比如程式在執行拼接操作前,需要先通過記憶體重分配來擴充套件底層陣列的空間大小——忘記則會產生緩衝區溢位
    • SDS 記錄自身長度,所以在進行操作時會進行相應的空間擴充套件再進行修改,不會出現緩衝區溢位
  • SDS 能減少修改字串時帶來的記憶體重分配次數

    • 空間預分配:對字串進行空間擴充套件時,擴充套件的記憶體比實際需要的多,這樣可以減少連續執行字串增長操作所需的記憶體重分配次數。
    • 惰性空間釋放:對字串進行縮短操作時,程式不立即使用記憶體重新分配來回收縮短後多餘的位元組,而是使用 alloc 屬性將這些位元組記錄下來,等待將來使用。
  • SDS 能實現二進位制安全

    • C 字串中的字元必須符合編碼格式,並且除了末尾外,中間不能包含空字元,否則會被誤認為是空字串結尾。這樣會使得 C 字串只能儲存文字資料,而不能儲存圖片、視訊等其他二進位制資料
    • SDS 的 buf 屬性則可以儲存多種二進位制資料,而且以 len 屬性表示的長度來判斷字串是否結束
  • SDS 相容部分 C 字串函式

    • 遵從每個字串都是以空字串結尾的慣例,可以重用 C 語言庫<string.h> 中的一部分函式

二、字典(Dict)

Redis 的字典使用雜湊表作為底層實現,程式碼位於 src/dict.h

2.1 字典的實現

2.1.1 雜湊表的結構定義

typedef struct dictht {
    //雜湊表陣列
    dictEntry **table;
    //雜湊表大小
    unsigned long size;
    //雜湊表大小掩碼,用於計算索引值
    unsigned long sizemask;
    //表示該雜湊表已有節點的數量
    unsigned long used;
} dictht;

如圖是一個大小為4的空雜湊表:

  • table 屬性:是一個陣列,陣列的每個元素都指向 dictEntry 結構的指標,每個 dictEntry 結構都儲存著一個鍵值對。其結構為:

    typedef struct dictEntry {
        //鍵key
        void *key;
        //值value,可以是指標、int64、double 等型別
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        //指向下一個 dictEntry 的指標,拉鍊法解決雜湊衝突
        struct dictEntry *next;
    } dictEntry;
    
  • size 屬性:記錄雜湊表的大小

  • sizemask 屬性:總是等於 size - 1

  • used 屬性:記錄雜湊表目前已有節點(鍵值對)的數量

2.1.2 字典的結構定義

Redis 中的字典程式碼在 src/dict.h 中

typedef struct dict {
    //指向 dictType 結構的指標
    dictType *type;
    //私有資料
    void *privdata;
    //雜湊表
    dictht ht[2];
    //索引,當 rehash 不在進行是,值為 01
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

一個沒有進行 rehash 的字典

  • type 屬性:指向 dictType 結構的指標,每個 dictType 結構儲存了一簇用於操作特定型別鍵值對的函式

    typedef struct dictType {
    
        // 計算雜湊值的函式
        unsigned int (*hashFunction)(const void *key);
    
        // 複製鍵的函式
        void *(*keyDup)(void *privdata, const void *key);
    
        // 複製值的函式
        void *(*valDup)(void *privdata, const void *obj);
    
        // 對比鍵的函式
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    
        // 銷燬鍵的函式
        void (*keyDestructor)(void *privdata, void *key);
    
        // 銷燬值的函式
        void (*valDestructor)(void *privdata, void *obj);
    
    } dictType;
    
  • privdata 屬性:儲存了需要傳給那些型別特定函式的可選引數

  • ht 屬性:包含兩個項的陣列,陣列中的每個項都是一個 dictht 雜湊表。一般情況,字典只使用 ht[0] 雜湊表,ht[1] 雜湊表在對 ht[0] 雜湊表進行 rehash 時使用。

  • rehashidx 屬性:如果沒有進行 rehash,它的值為 -1。

2.2 雜湊衝突

2.2.1 雜湊演算法

Redis 中計算雜湊值和索引值的方法為:

# 利用字典設定的雜湊函式,計算鍵key的雜湊值
hash = dict->type->hashFunction(key);
# 使用雜湊表的 sizemask 屬性和雜湊值來計算出索引值(h[x] 指的是 ht[0] 或者 ht[1])
index = hash & dict->ht[x].sizemask;

這裡的 hashFunction(key)是使用 MurmurHash 演算法來計算鍵的雜湊值,這種演算法的有點在於即使輸入的鍵是有規律的,演算法仍然能給出一個很好的隨機分佈性。演算法的計算速度也非常快。

2.2.2 雜湊衝突

前面提到過,Redis 中是通過拉鍊法來解決雜湊衝突,每個雜湊表節點都有一個 next 指標,多個雜湊表節點可以用 next 指標構成一個單向連結串列,來解決雜湊鍵衝突的問題。程式碼在 src/dict.c/dictAddRaw 中

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    if (dictIsRehashing(d)) _dictRehashStep(d);               // 1、執行rehash
    //如果索引等於 -1 表明字典中已經存在相同的 key
    if ((index = _dictKeyIndex(d, key)) == -1)                // 2、索引定位
        return NULL;
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];          // 3、根據是否 rehash ,選擇雜湊表
    entry = zmalloc(sizeof(*entry));                          // 4、分配記憶體空間,執行插入
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    dictSetKey(d, entry, key);                                // 5、設定鍵
    return entry;
}

其中 雜湊演算法的具體程式碼就在函式_dictKeyIndex()

static int _dictKeyIndex(dict *d, const void *key)
{
    unsigned int h, idx, table;
    dictEntry *he;
 
    if (_dictExpandIfNeeded(d) == DICT_ERR)                            // 1、rehash 判斷
        return -1;
    h = dictHashKey(d, key);                                           // 2、雜湊函式計算雜湊值
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;                               // 3、雜湊演算法計算索引值
        he = d->ht[table].table[idx];
        while(he) {                          
            if (key==he->key || dictCompareKeys(d, key, he->key))      // 4、查詢鍵是否已經存在
                return -1;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;                                // 5、rehash 判斷 
    }
    return idx;
}

2.2.3 rehash

同 Java 中的 HashMap 底層資料結構一樣,雜湊表儲存的鍵值對會增多或者減少,在 Redis 中是通過執行 rehash(重新雜湊) 來完成對錶的擴充套件和收縮。也就是讓雜湊表中的負載因子維持在一個合理的範圍中。這裡的負載因子是:load_factor = ht[0].used / ht[0].size

  • 擴充套件:1.伺服器正在執行 BGSAVE 或者 BGREWRITEAOF 命令並且雜湊表的負載因子大於等於 5 時;2.伺服器目前沒有執行 BGSAVE 或者 BGREWRITEAOF 命令並且雜湊表的負載因子大於等於1時,為 ht[1] 分配空間,大小是大於原 ht[0] 兩倍的2次冪

    • 從ht[0] 的值移動到 ht[1] 時,需要重新計算原 ht[0] 中元素的雜湊值和索引;插入到ht[1] 中,插一個刪除一個

    • ht[0] 中的元素全部遷移完後,釋放 ht[0],將新建的 ht[1] 設定為 ht[0] ,呼叫的是 dict.c/_dictExpandIfNeeded 函式:

      static int _dictExpandIfNeeded(dict *d)
      {
          if (dictIsRehashing(d)) return DICT_OK;
          if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);          // 大小為0需要設定初始雜湊表大小為4
          if (d->ht[0].used >= d->ht[0].size &&
              (dict_can_resize ||
               d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))                 // 負載因子超過5,執行 dictExpand
          {
              return dictExpand(d, d->ht[0].used*2);
          }
          return DICT_OK;
      }
      
  • 收縮:當雜湊表的負載因子小於 0.1 時,會對雜湊表執行收縮操作,同樣會分配 ht[1],大小等於 max(ht[0].used, DICT_HT_INTIAL_SIZE)。同樣也需要遷移元素,具體操作和擴充套件相同

2.2.4 漸進式 rehash

擴充套件或者收縮雜湊表是需要將 ht[0]中的鍵值對 rehash 到 ht[1] 中。因為當鍵值對數量非常多的時候,如果是一次性、集中式的完成大量的 rehash 動作,很可能會導致伺服器當機。所以需要漸進式的 rehash 來完成。漸進式 rehash 的步驟如下:

  1. 為 ht[1] 分配空間,在字典中的 rehashidx 變數設定為 0。這步程式碼在 dict.c/dictExpand 中實現

    int dictExpand(dict *d, unsigned long size)
    {
        dictht n;
        unsigned long realsize = _dictNextPower(size);                      // 找到比size大的最小的2的冪
        if (dictIsRehashing(d) || d->ht[0].used > size)
            return DICT_ERR;
        if (realsize == d->ht[0].size) return DICT_ERR;
     
        n.size = realsize;                                                 // 給ht[1]分配 realsize 的空間
        n.sizemask = realsize-1;
        n.table = zcalloc(realsize*sizeof(dictEntry*));
        n.used = 0;
        if (d->ht[0].table == NULL) {                                      // 處於初始化階段
            d->ht[0] = n;
            return DICT_OK;
        }
        d->ht[1] = n;
        d->rehashidx = 0;                                                  // rehashidx 設定為0,開始漸進式 rehash
        return DICT_OK;
    }
    
  2. 在 rehash 進行中對字典的 CRUD 操作時,除了這些制定的操作外。還會順帶將 ht[0] 的所有鍵值對被 rehash 到 ht[1] 中。 rehash 完成後,會將 rehashidx 屬性的值加1。

    • rehash 時的 CRUD 操作會在兩個雜湊表中進行,比如分別在兩個表中查詢,新增元素在 ht[1] 中新增
  3. 當 ht[0] 中所有鍵值對都被 rehash 到 ht[1] 後,將 rehashidx 屬性值設為 -1,rehash 操作完成。

三、壓縮列表(ZipList)

從本文開頭圖中可以看出,壓縮列表(ZipList)是列表鍵和雜湊鍵的底層實現原理。它是為了節約記憶體而開發出來的。一般用在一個列表中只含有很少的元素或者裡面的元素是小整數、長度較短的字串時, Redis 就會使用 ZipList 來做列表鍵的底層實現。

3.1 壓縮列表的構成

壓縮列表是由一系列特殊編碼的連續記憶體塊組成的順序型資料結構,一個壓縮列表可以包含多個節點,每個節點中可以儲存相應的資料型別(位元組陣列或者一個整數值)。如下圖

屬性 型別 長度 用途
zlbytes uint32_t 4 位元組 記錄整個壓縮列表佔用的記憶體位元組數:在對壓縮列表進行記憶體重分配, 或者計算 zlend 的位置時使用。
zltail uint32_t 4 位元組 記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組: 通過這個偏移量,程式無須遍歷整個壓縮列表就可以確定表尾節點的地址。
zllen uint16_t 2 位元組 記錄了壓縮列表包含的節點數量: 當這個屬性的值小於 UINT16_MAX65535)時, 這個屬性的值就是壓縮列表包含節點的數量; 當這個值等於 UINT16_MAX 時, 節點的真實數量需要遍歷整個壓縮列表才能計算得出。
entryX 列表節點 不定 壓縮列表包含的各個節點,節點的長度由節點儲存的內容決定。
zlend uint8_t 1 位元組 特殊值 0xFF (十進位制 255 ),用於標記壓縮列表的末端

如圖是一個壓縮列表例項:

  • zlbytes 屬性值為 0xd2(十進位制 210) ,表示壓縮列表總長度為 210 位元組
  • zltail 屬性值為 0xb3(十進位制 179),表示起始指標p 到末尾指標的偏移量是 179
  • zilen 屬性值為 0x5(十進位制 5),表示壓縮列表包含了五個 entry 節點
  • entry1~entry5 :表示各個列表
  • zlend 屬性值表示壓縮列表的末端

3.2 壓縮列表節點的構成

每個壓縮列表節點可以儲存一個位元組陣列或者一個整數值。其節點都由 previous_entry_length、encoding、content 三個屬性組成。如下圖:

  1. previous_entry_length 屬性: 以位元組為單位,記錄了壓縮列表中的前一個位元組的長度。

    • 若前一位元組的長度小於 254 位元組,則 previous_entry_length 的值為 1 位元組
    • 若前一位元組的長度大於等於 254 位元組,則 previous_entry_length 的值為5位元組:該屬性的第一個位元組被設定為 254,後面的四個位元組用於儲存超過 1 位元組的剩餘長度。
  2. encoding 屬性:記錄了節點的 content 屬性所儲存資料的型別以及長度

    • 1位元組、2位元組或者5位元組時,值的最高為00、01或者10的是位元組陣列編碼:這種編碼表示節點的 content 屬性儲存著位元組陣列。

      編碼 編碼長度 content 屬性儲存的值
      00bbbbbb 1 位元組 長度小於等於 63 位元組的位元組陣列。
      01bbbbbb xxxxxxxx 2 位元組 長度小於等於 16383 位元組的位元組陣列。
      10______ aaaaaaaa bbbbbbbb cccccccc dddddddd 5 位元組 長度小於等於 4294967295 的位元組陣列。
    • 1位元組,值的最高位以11開頭的整數編碼:整數值的型別和長度由編碼除去最高兩位之後的其他位記錄。

      編碼 編碼長度 content 屬性儲存的值
      11000000 1 位元組 int16_t 型別的整數。
      11010000 1 位元組 int32_t 型別的整數。
      11100000 1 位元組 int64_t 型別的整數。
      11110000 1 位元組 24 位有符號整數。
      11111110 1 位元組 8 位有符號整數。
      1111xxxx 1 位元組 使用這一編碼的節點沒有相應的 content 屬性, 因為編碼本身的 xxxx 四個位已經儲存了一個介於 012 之間的值, 所以它無須 content 屬性。
  3. content 屬性負責儲存節點的值,節點值可以是一個位元組陣列或者整數,值的型別和長度由節點的 encoding 屬性來決定。

    舉個儲存整數節點的例子:

    • encoding 是 11 開頭表示儲存的是整數型別
    • content 表示儲存節點值是 10086

為何 ZipList 省能記憶體?

  • 相對於普通的 list 而言,增加 encoding 屬性來選擇性讓位元組和整數型別進行儲存
  • 有了 previous_entry_length 屬性,遍歷元素時可以定位到下一個元素的精確位置,降低搜尋的時間複雜度

四、跳錶(ZSkipList)

跳錶是一種有序資料結構,它就是作為有序列表(Zset)的使用。而且相較於平衡樹比較更優雅,在 CRUD 等操作上可以在對數期望時間內完成。

如上圖所示,是一個跳躍表例項,最左側的是 zskiplist 結構,該結構包含的屬性有:

  • header屬性:指向跳躍表的表頭節點
  • tail屬性:指向跳躍表的表尾節點
  • level屬性:記錄跳躍表中層數最大節點的層數
  • length屬性:記錄跳躍表的長度,也就是跳躍表目前包含節點的數量

而右側的四個是 zskiplistNode 結構,該結構包含的屬性有:

  • level屬性:節點中用L1、L2、L3等等字樣標記節點的各層
  • backward屬性:節點中用 BW字樣標記節點的後退指標,它是指向當前結點的前一個節點
  • score屬性:節點中儲存的諸如1.0、2.0等等分值
  • obj屬性:節點中的 o1、o2等等是節點所儲存的成員物件

4.1 跳躍表結構定義

4.1.1 跳躍表的結構

如開始介紹的圖,zskiplist 結構程式碼在 redis.h/zskiplist 中,其定義如下:

typedef struct zskiplist {
    //表頭節點和表尾節點
    structz skiplistNode *header, *tail;
    //表中節點的數量
    unsigned long length;
    //表中層數最大節點的層數
    int level;
} zskiplist;

4.1.2 跳躍表節點的結構

其中 zskiplistNode 結構用於表示跳躍表節點,程式碼在 redis.h/zskiplistNode 中。

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    //物件屬性,指向了一個字串物件
    sds ele;
    //分值
    double score;
    //後退指標
    struct zskiplistNode *backward;
    //層level
    struct zskiplistLevel {
        //前進指標
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    } level[];
} zskiplistNode;

  • level 屬性:level 層中可以包含多個元素,層的數量越多,訪問其他節點的速度就越快。程式通過冪次定律(power law, 越大的數出現的概率越小)隨機生成一個介於1和32之間的值作為 level 陣列的大小作為層高:下圖是不同層高的節點

  • 前進和後退指標*forward * backforward):用於訪問遍歷跳躍表中所有節點的路徑

  • span跨度屬性:層的跨度是用於記錄兩個節點之間的距離:如上圖箭頭上的數字表示節點間的距離

  • 分值和成員(score 、sds ele)

    • 分值是一個 double 型別的浮點數,跳躍表中的所有節點都按分值從小到大排序
    • 成員物件(如o1、o2等等),它是一個指標,指向一個字串物件,這個物件儲存著一個SDS 值
      • 在同一個跳躍表中,各個節點儲存的成員物件必須是唯一的,分值可以相同。成員物件小的會排在前面(表頭)、成員物件較大的節點會排在後面(表尾)。還是這張圖,圖中 o1、o2和o3三個節點都儲存了相同的整數值 10086.0。但是成員物件的排序卻是 o1->o2->o3

五、整數集合(IntSet)

當一個集合中只有整數值元素,並且集合中的元素數量不多時,Redis則會使用整數集合作為集合鍵的底層實現

5.1 整數集合結構定義

intset 可以儲存型別為int16_t、int32_t或者int64_t的整數值,而且保證集合中不會出現重複元素。其結構程式碼在 intset.h/intset 中,下面以32為編碼方式為例:

typedef struct intset {
    //編碼方式
    uint32_t encoding;
    //集合包含的元素數量
    uint32_t length;
    //儲存元素的陣列
    int8_t contents[];
} intset;

以一個具體的整數集合為例:

  • encoding屬性值為 INTSET_ENC_INT16 :表示 contents 陣列是一個 16 位的陣列
  • length屬性值為5,表示contents 陣列中包含五個元素
  • contents 陣列,表示該陣列中從小到大儲存著五個元素

5.2 整數集合的升級

新元素的型別比現有的型別要長,比如說16位變成32位,在整數集合裡叫做升級(upgrade)。經過升級後,才能將新元素新增到整數集合中,升級整數集合並且新增新元素的步驟為:

  1. 根據新元素的型別,擴充套件底層陣列的空間大小,為新元素分配空間
  2. 將底層陣列現有的所有元素都轉換成新元素相同的型別,將型別轉換後的元素放置在正確的位置上。
  3. 將新元素新增到contents陣列裡面

集合也不會做降級操作,比如在原16位陣列中新加了一個32位元素,然後把這個新加的元素刪除後,整數集合也不會做降級操作。

六、快速連結串列(QuickList)

quicklist 結構時一種以 ziplist 為節點的雙端連結串列結構,整體上是一個連結串列,但是連結串列中的每一個節點都是一個 ziplist.

6.1 快速連結串列的結構

其程式碼結構為,在src/quicklist.h 中

我們先來看看quicklistNode 節點的結構:

typedef struct quicklistNode {
    //上一個quicklistNode節點
    struct quicklistNode *prev;
    //下一個quicklistNode節點
    struct quicklistNode *next;
    //資料指標,
    unsigned char *zl;
    //表示zl指向 ziplist 的總大小
    unsigned int sz;             /* ziplist size in bytes */
    //表示ziplist裡面包含的資料項個數
    unsigned int count : 16;     /* count of items in ziplist */
    //ziplist是否被壓縮,1是沒有,2是被壓縮
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    //預留欄位,目前是固定值,表示使用 ziplist 作為資料容器
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    //需要把資料暫時解壓,此時設定 recompress = 1,有機會再將資料重新壓縮
    unsigned int recompress : 1; /* was this node previous compressed? */
    //Redis 自動化測試時用
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    //擴充套件欄位,目前沒有被使用
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

再來看看 quicklistLZF 結構,表示一個被壓縮過的 ziplist,它的結構是:

typedef struct quicklistLZF {
    //壓縮後 ziplist 的大小
    unsigned int sz; /* LZF size in bytes*/
    //柔性陣列,存放壓縮後的 ziplist 位元組陣列
    char compressed[];
} quicklistLZF;

再看看最後的 quicklist 結構

typedef struct quicklist {
    //指向頭節點(quicklistnode 頭節點)
    quicklistNode *head;
    //指向尾節點(最右側節點)
    quicklistNode *tail;
    //所有ziplist 的數量
    unsigned long count;        /* total count of all entries in all ziplists */
    //quicklist節點的個數
    unsigned long len;          /* number of quicklistNodes */
    //設定ziplist 的大小,存放 list-max-ziplist-size 引數的值
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    //節點壓縮深度設定,存放 list-comress-depth 引數的值,為0的時候關閉
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    //尾部增加的書籤,只有在大量節點的多餘記憶體使用量可以忽略不計的情況,才分批迭代他們,不使用時不會增加記憶體開銷
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

6.2 快速連結串列的內部操作

6.2.1 插入操作

  1. 可以選擇在頭部或者尾部插入:
  • ziplist大小沒有超過限制,新資料直接插入到 ziplist 中
  • ziplist超過限制,那麼新建立一個 quicklistNode 節點,然後將新節點插入到 quicklist 雙向連結串列中
  1. 從中間位置插入:
  • 需要把當前 ziplist 分裂成兩個節點,然後在其中一個節點上插入資料

6.2.2 查詢操作

根據node 的個數找到對應的 ziplist,然後再呼叫 ziplist 的 index 就能成功找到

6.2.3 刪除操作

利用 quicklistDelRange 函式:返回1時表示成功刪除指定區間元素,返回0表示沒有刪除任何元素

在區間刪除時,會先找到 start 所在的 quicklistNode ,計算刪除的元素是否小於刪除的 count,如果不滿足刪除的個數,則會移動至下一個 quicklistNode 繼續刪除,依次迴圈直到刪除完成為止。

參考資料:

《Redis 設計與實現》

《Redis 開發與運維》

Redis資料結構——快速列表(quicklist)

https://pdai.tech/md/db/nosql-redis/db-redis-x-redis-ds.html

相關文章