Redis中的數據結構

1. 底層數據結構, 與Redis Value Type之間的關係

對於Redis的使用者來講, Redis做爲Key-Value型的內存數據庫, 其Value有多種類型.node

  1. String
  2. Hash
  3. List
  4. Set
  5. ZSet

這些Value的類型, 只是"Redis的用戶認爲的, Value存儲數據的方式". 而在具體實現上, 各個Type的Value到底如何存儲, 這對於Redis的使用者來講是不公開的.redis

舉個粟子: 使用下面的命令建立一個Key-Value算法

$ SET "Hello" "World"

對於Redis的使用者來講, Hello這個Key, 對應的Value是String類型, 其值爲五個ASCII字符組成的二進制數據. 但具體在底層實現上, 這五個字節是如何存儲的, 是不對用戶公開的. 即, Value的Type, 只是表象, 具體數據在內存中以何種數據結構存放, 這對於用戶來講是沒必要要了解的.數據庫

Redis對使用者暴露了五種Value Type, 其底層實現的數據結構有8種, 分別是:數組

  1. SDS - simple synamic string - 支持自動動態擴容的字節數組
  2. list - 平平無奇的鏈表
  3. dict - 使用雙哈希表實現的, 支持平滑擴容的字典
  4. zskiplist - 附加了後向指針的跳躍表
  5. intset - 用於存儲整數數值集合的自有結構
  6. ziplist - 一種實現上相似於TLV, 但比TLV複雜的, 用於存儲任意數據的有序序列的數據結構
  7. quicklist - 一種以ziplist做爲結點的雙鏈表結構, 實現的很是苟
  8. zipmap - 一種用於在小規模場合使用的輕量級字典結構

而銜接"底層數據結構"與"Value Type"的橋樑的, 則是Redis實現的另一種數據結構: redisObject. Redis中的Key與Value在表層都是一個redisObject實例, 故該結構有所謂的"類型", 便是ValueType. 對於每一種Value Type類型的redisObject, 其底層至少支持兩種不一樣的底層數據結構來實現. 以應對在不一樣的應用場景中, Redis的運行效率, 或內存佔用.安全

2. 底層數據結構

2.1 SDS - simple dynamic string

這是一種用於存儲二進制數據的一種結構, 具備動態擴容的特色. 其實現位於src/sds.hsrc/sds.c中, 其關鍵定義以下:服務器

typedef char *sds;

/* 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 */
    uint8_t alloc; /* excluding the header and null terminator */
    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[];
};

SDS的整體概覽以下圖:數據結構

sds

其中sdshdr是頭部, buf是真實存儲用戶數據的地方. 另外注意, 從命名上能看出來, 這個數據結構除了能存儲二進制數據, 顯然是用於設計做爲字符串使用的, 因此在buf中, 用戶數據後總跟着一個\0. 即圖中 "數據" + "\0" 是爲所謂的bufapp

SDS有五種不一樣的頭部. 其中sdshdr5實際並未使用到. 因此實際上有四種不一樣的頭部, 分別以下:dom

sdshdr

  1. len分別以uint8uint16uint32uint64表示用戶數據的長度(不包括末尾的\0)
  2. alloc分別以uint8uint16uint32uint64表示整個SDS, 除過頭部與末尾的\0, 剩餘的字節數.
  3. flag始終爲一字節, 以低三位標示着頭部的類型, 高5位未使用.

當在程序中持有一個SDS實例時, 直接持有的是數據區的頭指針, 這樣作的用意是: 經過這個指針, 向前偏一個字節, 就能取到flag, 經過判斷flag低三位的值, 能迅速判斷: 頭部的類型, 已用字節數, 總字節數, 剩餘字節數. 這也是爲何sds類型便是char *指針類型別名的緣由.

建立一個SDS實例有三個接口, 分別是:

// 建立一個不含數據的sds: 
//  頭部    3字節 sdshdr8
//  數據區  0字節
//  末尾    \0 佔一字節
sds sdsempty(void);
// 帶數據建立一個sds:
//  頭部    按initlen的值, 選擇最小的頭部類型
//  數據區  從入參指針init處開始, 拷貝initlen個字節
//  末尾    \0 佔一字節
sds sdsnewlen(const void *init, size_t initlen);
// 帶數據建立一個sds:
//  頭部    按strlen(init)的值, 選擇最小的頭部類型
//  數據區  入參指向的字符串中的全部字符, 不包括末尾 \0
//  末尾    \0 佔一字節
sds sdsnew(const char *init);
  1. 全部建立sds實例的接口, 都不會額外分配預留內存空間
  2. sdsnewlen用於帶二進制數據建立sds實例, sdsnew用於帶字符串建立sds實例. 接口返回的sds能夠直接傳入libc中的字符串輸出函數中進行操做, 因爲不管其中存儲的是用戶的二進制數據, 仍是字符串, 其末尾都帶一個\0, 因此至少調用libc中的字符串輸出函數是安全的.

在對SDS中的數據進行修改時, 若剩餘空間不足, 會調用sdsMakeRoomFor函數用於擴容空間, 這是一個很低級的API, 一般狀況下不該當由SDS的使用者直接調用. 其實現中核心的幾行以下:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    ...
    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    ...
}

能夠看到, 在擴充空間時

  1. 先保證至少有addlen可用
  2. 而後再進一步擴充, 在整體佔用空間不超過閾值SDS_MAC_PREALLOC時, 申請空間再翻一倍. 若整體空間已經超過了閾值, 則步進增加SDS_MAC_PREALLOC. 這個閾值的默認值爲 1024 * 1024

SDS也提供了接口用於移除全部未使用的內存空間. sdsRemoveFreeSpace, 該接口沒有間接的被任何SDS其它接口調用, 即默認狀況下, SDS不會自動回收預留空間. 在SDS的使用者須要節省內存時, 由使用者自行調用:

sds sdsRemoveFreeSpace(sds s);

總結:

  1. SDS除了是某些Value Type的底層實現, 也被大量使用在Redis內部, 用於替代C-Style字符串. 因此默認的建立SDS實例接口, 不分配額外的預留空間. 由於多數字符串在程序運行期間是不變的. 而對於變動數據區的API, 其內部則是調用了 sdsMakeRoomFor, 每一次擴充空間, 都會預留大量的空間. 這樣作的考量是: 若是一個SDS實例中的數據被變動了, 那麼頗有可能會在後續發生屢次變動.
  2. SDS的API內部不負責清除未使用的閒置內存空間, 由於內部API沒法判斷這樣作的合適時機. 即使是在操做數據區的時候致使數據區佔用內存減小時, 內部API也不會清除閒置內在空間. 清除閒置內存空間責任應當由SDS的使用者自行擔當.
  3. 用SDS替代C-Style字符串時, 因爲其頭部額外存儲了數據區的長度信息, 因此字符串的求長操做時間複雜度爲O(1)

2.2 list

這是普通的鏈表實現, 鏈表結點不直接持有數據, 而是經過void *指針來間接的指向數據. 其實現位於 src/adlist.hsrc/adlist.c中, 關鍵定義以下:

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

其內存佈局以下圖所示:

list

這是一個平平無奇的鏈表的實現. list在Redis除了做爲一些Value Type的底層實現外, 還普遍用於Redis的其它功能實現中, 做爲一種數據結構工具使用. 在list的實現中, 除了基本的鏈表定義外, 還額外增長了:

  1. 迭代器listIter的定義, 與相關接口的實現.
  2. 因爲list中的鏈表結點自己並不直接持有數據, 而是經過value字段, 以void *指針的形式間接持有, 因此數據的生命週期並不徹底與鏈表及其結點一致. 這給了list的使用者至關大的靈活性. 好比能夠多個結點持有同一份數據的地址. 但與此同時, 在對鏈表進行銷燬, 結點複製以及查找匹配時, 就須要list的使用者將相關的函數指針賦值於list.duplist.freelist.match字段.

2.3 dict

dict是Redis底層數據結構中實現最爲複雜的一個數據結構, 其功能相似於C++標準庫中的std::unordered_map, 其實現位於 src/dict.h 與 src/dict.c中, 其關鍵定義以下:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {
    uint64_t (*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;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;

其內存佈局以下所示:

dict

  1. dict中存儲的鍵值對, 是經過dictEntry這個結構間接持有的, k經過指針間接持有鍵, v經過指針間接持有值. 注意, 若值是整數值的話, 是直接存儲在v字段中的, 而不是間接持有. 同時next指針用於指向, 在bucket索引值衝突時, 以鏈式方式解決衝突, 指向同索引的下一個dictEntry結構.
  2. 傳統的哈希表實現, 是一塊連續空間的順序表, 表中元素便是結點. 在dictht.table中, 結點自己是散佈在內存中的, 順序表中存儲的是dictEntry的指針
  3. 哈希表便是dictht結構, 其經過table字段間接的持有順序表形式的bucket, bucket的容量存儲在size字段中, 爲了加速將散列值轉化爲bucket中的數組索引, 引入了sizemask字段, 計算指定鍵在哈希表中的索引時, 執行的操做相似於dict->type->hashFunction(鍵) & dict->ht[x].sizemask. 從這裏也能夠看出來, bucket的容量適宜於爲2的冪次, 這樣計算出的索引值能覆蓋到全部bucket索引位.
  4. dict即爲字典. 其中type字段中存儲的是本字典使用到的各類函數指針, 包括散列函數, 鍵與值的複製函數, 釋放函數, 以及鍵的比較函數. privdata是用於存儲用戶自定義數據. 這樣, 字典的使用者能夠最大化的自定義字典的實現, 經過自定義各類函數實現, 以及能夠附帶私有數據, 保證了字典有很大的調優空間.
  5. 字典爲了支持平滑擴容, 定義了ht[2]這個數組字段. 其用意是這樣的:
    1. 通常狀況下, 字典dict僅持有一個哈希表dictht的實例, 即整個字典由一個bucket實現.
    2. 隨着插入操做, bucket中出現衝突的機率會愈來愈大, 當字典中存儲的結點數目, 與bucket數組長度的比值達到一個閾值(1:1)時, 字典爲了緩解性能降低, 就須要擴容
    3. 擴容的操做是平滑的, 即在擴容時, 字典會持有兩個dictht的實例, ht[0]指向舊哈希表, ht[1]指向擴容後的新哈希表. 平滑擴容的重點在於兩個策略:
      1. 後續每一次的插入, 替換, 查找操做, 都插入到ht[1]指向的哈希表中
      2. 每一次插入, 替換, 查找操做執行時, 會將舊錶ht[0]中的一個bucket索引位持有的結點鏈表, 遷移到ht[1]中去. 遷移的進度保存在rehashidx這個字段中.在舊錶中因爲衝突而被連接在同一索引位上的結點, 遷移到新表後, 可能會散佈在多個新表索引中去.
      3. 當遷移完成後, ht[0]指向的舊錶會被釋放, 以後會將新表的持有權轉交給ht[0], 再重置ht[1]指向NULL
  6. 這種平滑擴容的優勢有兩個:
    1. 平滑擴容過程當中, 全部結點的實際數據, 即dict->ht[0]->table[rehashindex]->kdict->ht[0]->table[rehashindex]->v分別指向的實際數據, 內存地址都不會變化. 沒有發生鍵數據與值數據的拷貝或移動, 擴容整個過程僅是各類指針的操做. 速度很是快
    2. 擴容操做是步進式的, 這保證任何一次插入操做都是順暢的, dict的使用者是無感知的. 若擴容是一次性的, 當新舊bucket容量特別大時, 遷移全部結點必然會致使耗時陡增.

除了字典自己的實現外, 其中還順帶實現了一個迭代器, 這個迭代器中有字段safe以標示該迭代器是"安全迭代器"仍是"非安全迭代器", 所謂的安全與否, 指是的這種場景:
設想在運行迭代器的過程當中, 字典正處於平滑擴容的過程當中. 在平滑擴容的過程當中時, 舊錶一個索引位上的, 由衝突而鏈起來的多個結點, 遷移到新表後, 可能會散佈到新表的多個索引位上. 且新的索引位的值可能比舊的索引位要低.

遍歷操做的重點是, 保證在迭代器遍歷操做開始時, 字典中持有的全部結點, 都會被遍歷到. 而若在遍歷過程當中, 一個未遍歷的結點, 從舊錶遷移到新表後, 索引值減少了, 那麼就可能會致使這個結點在遍歷過程當中被遺漏.

因此, 所謂的"安全"迭代器, 其在內部實現時: 在迭代過程當中, 若字典正處於平滑擴容過程, 則暫停結點遷移, 直至迭代器運行結束. 這樣雖然不能保證在迭代過程當中插入的結點會被遍歷到, 但至少保證在迭代起始時, 字典中持有的全部結點都會被遍歷到.

這也是爲何dict結構中有一個iterators字段的緣由: 該字段記錄了運行於該字典上的安全迭代器的數目. 若該數目不爲0, 字典是不會繼續進行結點遷移平滑擴容的.

下面是字典的擴容操做中的核心代碼, 咱們以插入操做引發的擴容爲例:

先是插入操做的外部邏輯:

  1. 若是插入時, 字典正處於平滑擴容過程當中, 那麼不管本次插入是否成功, 先遷移一個bucket索引中的結點至新表
  2. 在計算新插入結點鍵的bucket索引值時, 內部會探測哈希表是否須要擴容(若當前不在平滑擴容過程當中)
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);          // 調用dictAddRaw

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d); // 若在平滑擴容過程當中, 先步進遷移一個bucket索引

    /* Get the index of the new element, or -1 if
     * the element already exists. */

    // 在計算鍵在bucket中的索引值時, 內部會檢查是否須要擴容
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

下面是計算bucket索引值的函數, 內部會探測該哈希表是否須要擴容, 若是須要擴容(結點數目與bucket數組長度比例達到1:1), 就使字典進入平滑擴容過程:

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR) // 探測是否須要擴容, 若是須要, 則開始擴容
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK; // 若是正在擴容過程當中, 則什麼也不作

    /* If the hash table is empty expand it to the initial size. */
    // 若字典中本無元素, 則初始化字典, 初始化時的bucket數組長度爲4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    // 若字典中元素的個數與bucket數組長度比值大於1:1時, 則調用dictExpand進入平滑擴容狀態
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* the new hash table */  // 新建一個dictht結構
    unsigned long realsize = _dictNextPower(size);  

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));// 初始化dictht下的table, 即bucket數組
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    // 如果新字典初始化, 直接把dictht結構掛在ht[0]中
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    // 不然, 把新dictht結構掛在ht[1]中, 並開啓平滑擴容(置rehashidx爲0, 字典處於非擴容狀態時, 該字段值爲-1)
    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

下面是平滑擴容的實現:

static void _dictRehashStep(dict *d) {
    // 若字典上還運行着安全迭代器, 則不遷移結點
    // 不然每次遷移一箇舊bucket索引上的全部結點
    if (d->iterators == 0) dictRehash(d,1); 
}

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        // 在舊bucket中, 找到下一個非空的索引位
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // 取出該索引位上的結點鏈表
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 把全部結點遷移到新bucket中去
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    // 檢查是否舊錶中的全部結點都被遷移到了新表
    // 若是是, 則置先釋放原舊bucket數組, 再置ht[1]爲ht[0]
    // 最後再置rehashidx=-1, 以示字典不處於平滑擴容狀態
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

總結:

  1. 字典的實現很複雜, 主要是實現了平滑擴容邏輯
  2. 用戶數據均是以指針形式間接由dictEntry結構持有, 故在平滑擴容過程當中, 不涉及用戶數據的拷貝
  3. 有安全迭代器可用, 安全迭代器保證, 在迭代起始時, 字典中的全部結點, 都會被迭代到, 即便在迭代過程當中對字典有插入操做
  4. 字典內部使用的默認散列函數其實也很是有講究, 不過限於篇幅, 這裏不展開講. 而且字典的實現給了使用者很是大的靈活性(dictType結構與dict.privdata字段), 對於一些特定場合使用的鍵數據, 用戶能夠自行選擇更高效更特定化的散列函數

2.4 zskiplist

zskiplist是Redis實現的一種特殊的跳躍表. 跳躍表是一種基於線性表實現簡單的搜索結構, 其最大的特色就是: 實現簡單, 性能能逼近各類搜索樹結構. 血統純正的跳躍表的介紹在維基百科中便可查閱. 在Redis中, 在原版跳躍表的基礎上, 進行了一些小改動, 便是如今要介紹的zskiplist結構.

其定義在src/server.h中, 以下:

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

其內存佈局以下圖:

zskiplist

zskiplist的核心設計要點爲:

  1. 頭結點不持有任何數據, 且其level[]的長度爲32
  2. 每一個結點, 除了持有數據的ele字段, 還有一個字段score, 其標示着結點的得分, 結點之間憑藉得分來判斷前後順序, 跳躍表中的結點按結點的得分升序排列.
  3. 每一個結點持有一個backward指針, 這是原版跳躍表中所沒有的. 該指針指向結點的前一個緊鄰結點.
  4. 每一個結點中最多持有32個zskiplistLevel結構. 實際數量在結點建立時, 按冪次定律隨機生成(不超過32). 每一個zskiplistLevel中有兩個字段.
    1. forward字段指向比本身得分高的某個結點(不必定是緊鄰的), 而且, 若當前zskiplistLevel實例在level[]中的索引爲X, 則其forward字段指向的結點, 其level[]字段的容量至少是X+1. 這也是上圖中, 爲何forward指針老是畫的水平的緣由.
    2. span字段表明forward字段指向的結點, 距離當前結點的距離. 緊鄰的兩個結點之間的距離定義爲1.
  5. zskiplist中持有字段level, 用以記錄全部結點(除過頭結點外), level[]數組最長的長度.

跳躍表主要用於, 在給定一個分值的狀況下, 查找與該分值最接近的結點. 搜索時, 僞代碼以下:

int level = zskiplist->level - 1;
zskiplistNode p = zskiplist->head;

while(1 && p)
{
    zskiplistNode q = (p->level)[level]->forward:
    if(q->score > 分值)
    {
        if(level > 0)
        {
            level--;
        }
        else
        {
            return :
                q爲整個跳躍表中, 分值大於指定分值的第一個結點
                q->backward爲整個跳躍表中, 分值小於或等於指定分值的最後一個結點
        }
    }
    else
    {
        p = q;
    }
}

跳躍表的實現比較簡單, 最複雜的操做便是插入與刪除結點, 須要仔細處理鄰近結點的全部level[]中的全部zskiplistLevel結點中的forwardspan的值的變動.

另外, 關於新建立的結點, 其level[]數組長度的隨機算法, 在接口zslInsert的實現中, 核心代碼片段以下:

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    //...

    level = zslRandomLevel();   // 隨機生成新結點的, level[]數組的長度
        if (level > zsl->level) {   
        // 若生成的新結點的level[]數組的長度比當前表中全部結點的level[]的長度都大
        // 那麼頭結點中須要新增幾個指向該結點的指針
        // 並刷新ziplist中的level字段
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    x = zslCreateNode(level,score,ele); // 建立新結點
    //... 執行插入操做
}

// 按冪次定律生成小於32的隨機數的函數
// 宏 ZSKIPLIST_MAXLEVEL 的定義爲32, 宏 ZSKIPLIST_P 被設定爲 0.25
// 即 
//      level == 1的機率爲 75%
//      level == 2的機率爲 75% * 25%
//      level == 3的機率爲 75% * 25% * 25%
//      ...
//      level == 31的機率爲 0.75 * 0.25^30
//      而
//      level == 32的機率爲 0.75 * sum(i = 31 ~ +INF){ 0.25^i }
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

2.5 intset

這是一個用於存儲在序的整數的數據結構, 也底層數據結構中最簡單的一個, 其定義與實如今src/intest.hsrc/intset.c中, 關鍵定義以下:

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

inset結構中的encoding的取值有三個, 分別是宏INTSET_ENC_INT16INTSET_ENC_INT32INTSET_ENC_INT64length表明其中存儲的整數的個數, contents指向實際存儲數值的連續內存區域. 其內存佈局以下圖所示:

intset

  1. intset中各字段, 包括contents中存儲的數值, 都是以主機序(小端字節序)存儲的. 這意味着Redis若運行在PPC這樣的大端字節序的機器上時, 存取數據都會有額外的字節序轉換開銷
  2. encoding == INTSET_ENC_INT16時, contents中以int16_t的形式存儲着數值. 相似的, 當encoding == INTSET_ENC_INT32時, contents中以int32_t的形式存儲着數值.
  3. 但凡是有一個數值元素的值超過了int32_t的取值範圍, 整個intset都要進行升級, 即全部的數值都須要以int64_t的形式存儲. 顯然升級的開銷是很大的.
  4. intset中的數值是以升序排列存儲的, 插入與刪除的複雜度均爲O(n). 查找使用二分法, 複雜度爲O(log_2(n))
  5. intset的代碼實現中, 不預留空間, 即每一次插入操做都會調用zrealloc接口從新分配內存. 每一次刪除也會調用zrealloc接口縮減佔用的內存. 省是省了, 但內存操做的時間開銷上升了.
  6. intset的編碼方式一經升級, 不會再降級.

總之, intset適合於以下數據的存儲:

  1. 全部數據都位於一個穩定的取值範圍中. 好比均位於int16_tint32_t的取值範圍中
  2. 數據穩定, 插入刪除操做不頻繁. 能接受O(lgn)級別的查找開銷

2.6 ziplist

ziplist是Redis底層數據結構中, 最苟的一個結構. 它的設計宗旨就是: 省內存, 從牙縫裏省內存. 設計思路和TLV一致, 但爲了從牙縫裏節省內存, 作了不少額外工做.

ziplist的內存佈局與intset同樣: 就是一塊連續的內存空間. 但區域劃分比較複雜, 概覽以下圖:

ziplist_overall

  1. intset同樣, ziplist中的全部值都是以小端序存儲的
  2. zlbytes字段的類型是uint32_t, 這個字段中存儲的是整個ziplist所佔用的內存的字節數
  3. zltail字段的類型是uint32_t, 它指的是ziplist中最後一個entry的偏移量. 用於快速定位最後一個entry, 以快速完成pop等操做
  4. zllen字段的類型是uint16_t, 它指的是整個ziplitentry的數量. 這個值只佔16位, 因此蛋疼的地方就來了: 若是ziplistentry的數目小於65535, 那麼該字段中存儲的就是實際entry的值. 若等於或超過65535, 那麼該字段的值固定爲65535, 但實際數量須要一個個entry的去遍歷全部entry才能獲得.
  5. zlend是一個終止字節, 其值爲全F, 即0xffziplist保證任何狀況下, 一個entry的首字節都不會是255

在畫圖展現entry的內存佈局以前, 先講一下entry中都存儲了哪些信息:

  1. 每一個entry中存儲了它前一個entry所佔用的字節數. 這樣支持ziplist反向遍歷.
  2. 每一個entry用單獨的一塊區域, 存儲着當前結點的類型: 所謂的類型, 包括當前結點存儲的數據是什麼(二進制, 仍是數值), 如何編碼(若是是數值, 數值如何存儲, 若是是二進制數據, 二進制數據的長度)
  3. 最後就是真實的數據了

entry的內存佈局以下所示:

ziplist_entry_1

prevlen便是"前一個entry所佔用的字節數", 它自己是一個變長字段, 規約以下:

  1. 若前一個entry佔用的字節數小於 254, 則prevlen字段佔一字節
  2. 若前一個entry佔用的字節數等於或大於 254, 則prevlen字段佔五字節: 第一個字節值爲 254, 即0xfe, 另外四個字節, 以uint32_t存儲着值.

encoding字段的規約就複雜了許多

  1. 若數據是二進制數據, 且二進制數據長度小於64字節(不包括64), 那麼encoding佔一字節. 在這一字節中, 高兩位值固定爲0, 低六位值以無符號整數的形式存儲着二進制數據的長度. 即 00xxxxxx, 其中低六位bitxxxxxx是用二進制保存的數據長度.
  2. 若數據是二進制數據, 且二進制數據長度大於或等於64字節, 但小於16384(不包括16384)字節, 那麼encoding佔用兩個字節. 在這兩個字節16位中, 第一個字節的高兩位固定爲01, 剩餘的14個位, 以小端序無符號整數的形式存儲着二進制數據的長度, 即 01xxxxxx, yyyyyyyy, 其中yyyyyyyy是高八位, xxxxxx是低六位.
  3. 若數據是二進制數據, 且二進制數據的長度大於或等於16384字節, 但小於2^32-1字節, 則encoding佔用五個字節. 第一個字節是固定值10000000, 剩餘四個字節, 按小端序uint32_t的形式存儲着二進制數據的長度. 這也是ziplist能存儲的二進制數據的最大長度, 超過2^32-1字節的二進制數據, ziplist沒法存儲.
  4. 若數據是整數值, 則encodingdata的規約以下:
    1. 首先, 全部存儲數值的entry, 其encoding都僅佔用一個字節. 而且最高兩位均是11
    2. 若數值取值範圍位於[0, 12]中, 則encodingdata擠在同一個字節中. 即爲1111 0001~1111 1101, 高四位是固定值, 低四位的值從00011101, 分別表明 0 ~ 12這十五個數值
    3. 若數值取值範圍位於[-128, -1] [13, 127]中, 則encoding == 0b 1111 1110. 數值存儲在緊鄰的下一個字節, 以int8_t形式編碼
    4. 若數值取值範圍位於[-32768, -129] [128, 32767]中, 則encoding == 0b 1100 0000. 數值存儲在緊鄰的後兩個字節中, 以小端序int16_t形式編碼
    5. 若數值取值範圍位於[-8388608, -32769] [32768, 8388607]中, 則encoding == 0b 1111 0000. 數值存儲在緊鄰的後三個字節中, 以小端序存儲, 佔用三個字節.
    6. 若數值取值範圍位於[-2^31, -8388609] [8388608, 2^31 - 1]中, 則encoding == 0b 1101 0000. 數值存儲在緊鄰的後四個字節中, 以小端序int32_t形式編碼
    7. 若數值取值均不在上述範圍, 但位於int64_t所能表達的範圍內, 則encoding == 0b 1110 0000, 數值存儲在緊鄰的後八個字節中, 以小端序int64_t形式編碼

在大規模數值存儲中, ziplist幾乎不浪費內存空間, 其苟的程序到達了字節級別, 甚至對於[0, 12]區間的數值, 連data裏的那一個字節也要省下來. 顯然, ziplist是一種特別節省內存的數據結構, 但它的缺點也十分明顯:

  1. intset同樣, ziplist也不預留內存空間, 而且在移除結點後, 也是當即縮容, 這表明每次寫操做都會進行內存分配操做.
  2. ziplist最蛋疼的一個問題是: 結點若是擴容, 致使結點佔用的內存增加, 而且超過254字節的話, 可能會致使鏈式反應: 其後一個結點的entry.prevlen須要從一字節擴容至五字節. 最壞狀況下, 第一個結點的擴容, 會致使整個ziplist表中的後續全部結點的entry.prevlen字段擴容. 雖然這個內存重分配的操做依然只會發生一次, 但代碼中的時間複雜度是o(N)級別, 由於鏈式擴容只能一步一步的計算. 但這種狀況的機率十分的小, 通常狀況下鏈式擴容能連鎖反映五六次就很不幸了. 之因此說這是一個蛋疼問題, 是由於, 這樣的壞場景下, 其實時間複雜度並不高: 依次計算每一個entry新的空間佔用, 也就是o(N), 整體佔用計算出來後, 只執行一次內存重分配, 與對應的memmove操做, 就能夠了. 蛋疼說的是: 代碼特別難寫, 難讀. 下面放一段處理插入結點時處理鏈式反應的代碼片段, 你們自行感覺一下:
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; /* initialized to avoid warning. Using a value
                                    that is easy to see if for some reason
                                    we use it uninitialized. */
    zlentry tail;

    /* Find out prevlen for the entry that is inserted. */
    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);
        }
    }

    /* See if the entry can be encoded */
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipStoreEntryEncoding will use the
         * string length to figure out how to encode it. */
        reqlen = slen;
    }
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);

    /* When the insert position is not equal to the tail, we need to
     * make sure that the next entry can hold this entry's length in
     * its prevlen field. */
    int forcelarge = 0;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }

    /* Store offset because a realloc may change the address of zl. */
    offset = p-zl;
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;

    /* Apply memory move when necessary and update tail offset. */
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes */
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);

        /* Update offset for tail */
        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. */
        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 {
        /* This element will be the new tail. */
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    /* When nextdiff != 0, the raw length of the next entry has changed, so
     * we need to cascade the update throughout the ziplist */
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    /* Write the entry */
    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;
}

unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
    size_t offset, noffset, extra;
    unsigned char *np;
    zlentry cur, next;

    while (p[0] != ZIP_END) {
        zipEntry(p, &cur);
        rawlen = cur.headersize + cur.len;
        rawlensize = zipStorePrevEntryLength(NULL,rawlen);

        /* Abort if there is no next entry. */
        if (p[rawlen] == ZIP_END) break;
        zipEntry(p+rawlen, &next);

        /* Abort when "prevlen" has not changed. */
        if (next.prevrawlen == rawlen) break;

        if (next.prevrawlensize < rawlensize) {
            /* The "prevlen" field of "next" needs more bytes to hold
             * the raw length of "cur". */
            offset = p-zl;
            extra = rawlensize-next.prevrawlensize;
            zl = ziplistResize(zl,curlen+extra);
            p = zl+offset;

            /* Current pointer and offset for next element. */
            np = p+rawlen;
            noffset = np-zl;

            /* Update tail offset when next element is not the tail element. */
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                    intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }

            /* Move the tail to the back. */
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipStorePrevEntryLength(np,rawlen);

            /* Advance the cursor */
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
                /* This would result in shrinking, which we want to avoid.
                 * So, set "rawlen" in the available bytes. */
                zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
            } else {
                zipStorePrevEntryLength(p+rawlen,rawlen);
            }

            /* Stop here, as the raw length of "next" has not changed. */
            break;
        }
    }
    return zl;
}

這種代碼的特色就是: 最好由做者去維護, 最好一次性寫對. 由於讀起來真的費勁, 改起來也很費勁.

2.7 quicklist

若是說ziplist是整個Redis中爲了節省內存, 而寫的最苟的數據結構, 那麼稱quicklist就是在最苟的基礎上, 再苟了一層. 這個結構是Redis在3.2版本後新加的, 在3.2版本以前, 咱們能夠講, dict是最複雜的底層數據結構, ziplist是最苟的底層數據結構. 在3.2版本以後, 這兩個記錄被雙雙刷新了.

這是一種, 以ziplist爲結點的, 雙端鏈表結構. 宏觀上, quicklist是一個鏈表, 微觀上, 鏈表中的每一個結點都是一個ziplist.

它的定義與實現分別在src/quicklist.hsrc/quicklist.c中, 其中關鍵定義以下:

/* Node, quicklist, and Iterator are the only data structures used currently. */

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
 * 'sz' is byte length of 'compressed' field.
 * 'compressed' is LZF data with total (compressed) length 'sz'
 * NOTE: uncompressed length is stored in quicklistNode->sz.
 * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;

/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
 * 'count' is the number of total entries.
 * 'len' is the number of quicklist nodes.
 * 'compress' is: -1 if compression disabled, otherwise it's the number
 *                of quicklistNodes to leave uncompressed at ends of quicklist.
 * 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

typedef struct quicklistIter {
    const quicklist *quicklist;
    quicklistNode *current;
    unsigned char *zi;
    long offset; /* offset in current ziplist */
    int direction;
} quicklistIter;

typedef struct quicklistEntry {
    const quicklist *quicklist;
    quicklistNode *node;
    unsigned char *zi;
    unsigned char *value;
    long long longval;
    unsigned int sz;
    int offset;
} quicklistEntry;

這裏定義了五個結構體:

  1. quicklistNode, 宏觀上, quicklist是一個鏈表, 這個結構描述的就是鏈表中的結點. 它經過zl字段持有底層的ziplist. 簡單來說, 它描述了一個ziplist實例
  2. quicklistLZFziplist是一段連續的內存, 用LZ4算法壓縮後, 就能夠包裝成一個quicklistLZF結構. 是否壓縮quicklist中的每一個ziplist實例是一個可配置項. 若這個配置項是開啓的, 那麼quicklistNode.zl字段指向的就不是一個ziplist實例, 而是一個壓縮後的quicklistLZF實例
  3. quicklist. 這就是一個雙鏈表的定義. head, tail分別指向頭尾指針. len表明鏈表中的結點. count指的是整個quicklist中的全部ziplist中的entry的數目. fill字段影響着每一個鏈表結點中ziplist的最大佔用空間, compress影響着是否要對每一個ziplist以LZ4算法進行進一步壓縮以更節省內存空間.
  4. quicklistIter是一個迭代器
  5. quicklistEntry是對ziplist中的entry概念的封裝. quicklist做爲一個封裝良好的數據結構, 不但願使用者感知到其內部的實現, 因此須要把ziplist.entry的概念從新包裝一下.

quicklist的內存佈局圖以下所示:

quicklist

下面是有關quicklist的更多額外信息:

  1. quicklist.fill的值影響着每一個鏈表結點中, ziplist的長度.
    1. 當數值爲負數時, 表明以字節數限制單個ziplist的最大長度. 具體爲:
      1. -1 不超過4kb
      2. -2 不超過 8kb
      3. -3 不超過 16kb
      4. -4 不超過 32kb
      5. -5 不超過 64kb
    2. 當數值爲正數時, 表明以entry數目限制單個ziplist的長度. 值即爲數目. 因爲該字段僅佔16位, 因此以entry數目限制ziplist的容量時, 最大值爲2^15個
  2. quicklist.compress的值影響着quicklistNode.zl字段指向的是原生的ziplist, 仍是通過壓縮包裝後的quicklistLZF
    1. 0 表示不壓縮, zl字段直接指向ziplist
    2. 1 表示quicklist的鏈表頭尾結點不壓縮, 其他結點的zl字段指向的是通過壓縮後的quicklistLZF
    3. 2 表示quicklist的鏈表頭兩個, 與末兩個結點不壓縮, 其他結點的zl字段指向的是通過壓縮後的quicklistLZF
    4. 以此類推, 最大值爲2^16
  3. quicklistNode.encoding字段, 以指示本鏈表結點所持有的ziplist是否通過了壓縮. 1表明未壓縮, 持有的是原生的ziplist2表明壓縮過
  4. quicklistNode.container字段指示的是每一個鏈表結點所持有的數據類型是什麼. 默認的實現是ziplist, 對應的該字段的值是2, 目前Redis沒有提供其它實現. 因此實際上, 該字段的值恆爲2
  5. quicklistNode.recompress字段指示的是當前結點所持有的ziplist是否通過了解壓. 若是該字段爲1即表明以前被解壓過, 且須要在下一次操做時從新壓縮.

quicklist的具體實現代碼篇幅很長, 這裏就不貼代碼片段了, 從內存佈局上也能看出來, 因爲每一個結點持有的ziplist是有上限長度的, 因此在與操做時要考慮的分支狀況比較多. 想一想都蛋疼.

quicklist有本身的優勢, 也有缺點, 對於使用者來講, 其使用體驗相似於線性數據結構, list做爲最傳統的雙鏈表, 結點經過指針持有數據, 指針字段會耗費大量內存. ziplist解決了耗費內存這個問題. 但引入了新的問題: 每次寫操做整個ziplist的內存都須要重分配. quicklist在二者之間作了一個平衡. 而且使用者能夠經過自定義quicklist.fill, 根據實際業務狀況, 經驗主義調參.

2.8 zipmap

dict做爲字典結構, 優勢不少, 擴展性強悍, 支持平滑擴容等等, 但對於字典中的鍵值均爲二進制數據, 且長度都很小時, dict的中的一坨指針會浪費很多內存, 所以Redis又實現了一個輕量級的字典, 即爲zipmap.

zipmap適合使用的場合是:

  1. 鍵值對量不大, 單個鍵, 單個值長度小
  2. 鍵值均是二進制數據, 而不是複合結構或複雜結構. dict支持各類嵌套, 字典自己並不持有數據, 而僅持有數據的指針. 但zipmap是直接持有數據的.

zipmap的定義與實如今src/zipmap.hsrc/zipmap.c兩個文件中, 其定義與實現均未定義任何struct結構體, 由於zipmap的內存佈局就是一塊連續的內存空間. 其內存佈局以下所示:

zipmap

  1. zipmap起始的第一個字節存儲的是zipmap中鍵值對的個數. 若是鍵值對的個數大於254的話, 那麼這個字節的值就是固定值254, 真實的鍵值對個數須要遍歷才能得到.
  2. zipmap的最後一個字節是固定值0xFF
  3. zipmap中的每個鍵值對, 稱爲一個entry, 其內存佔用如上圖, 分別六部分:
    1. len_of_key, 一字節或五字節. 存儲的是鍵的二進制長度. 若是長度小於254, 則用1字節存儲, 不然用五個字節存儲, 第一個字節的值固定爲0xFE, 後四個字節以小端序uint32_t類型存儲着鍵的二進制長度.
    2. key_data爲鍵的數據
    3. len_of_val, 一字節或五字節, 存儲的是值的二進制長度. 編碼方式同len_of_key
    4. len_of_free, 固定值1字節, 存儲的是entry中未使用的空間的字節數. 未使用的空間即爲圖中的free, 它通常是因爲鍵值對中的值被替換髮生的. 好比, 鍵值對hello <-> word被修改成hello <-> w後, 就空了四個字節的閒置空間
    5. val_data, 爲值的數據
    6. free, 爲閒置空間. 因爲len_of_free的值最大隻能是254, 因此若是值的變動致使閒置空間大於254的話, zipmap就會回收內存空間.

3. 膠水層 redisObject

銜接底層數據結構, 與五種Value Type之間的橋樑就是redisObject這個結構. 該結構的關鍵定義以下(位於src/server.h中):

/*-----------------------------------------------------------------------------
 * Data types
 *----------------------------------------------------------------------------*/

/* A redis object, that is a type able to hold a string / list / set */

/* The actual Redis Object */
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */

#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */

#define OBJ_SHARED_REFCOUNT INT_MAX
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

redisObject的內存佈局以下:

redisObject

從定義上來看, redisObject有:

  1. 與Value Type一致的Object Type, 即type字段
  2. 特定的Object Encoding, 即encoding字段, 代表對象底層使用的數據結構類型
  3. 記錄最末一次訪問時間的lru字段
  4. 引用計數refcount
  5. 指向底層數據結構實例的ptr字段

redisObject的通用操做API以下:

API 功能
char *strEncoding(int encoding) 返回各類編碼的可讀字符串表達
void decrRefCount(robj *o); 引用計數-1. 若減後引用計數會降爲0, 則會自動調用 freeXXXObject函數釋放對象
void decrRefCountVoid(void *o); 功能同decrRefCount, 只不過接收的是void * 型參數
void incrRefCount(robj *o); 引用計數+1
robj *makeObjectShared(robj *o); 將對象置爲"全局共享對象", 所謂的"全局只讀共享對象", 有如下特徵
0. 內部引用計數爲 INT_MAX
0. 引用計數操做函數對其不起做用
0. 多純種共享讀是安全的, 不須要加鎖
0. 禁止寫操做
robj *resetRefCount(robj *obj); 將引用計數置爲0, 但不會調用freeXXXObject函數釋放對象
robj *createObject(int type, void *ptr); 建立一個對象, 對象類型由參數指定, 對象底層編碼指定爲RAW, 底層數據由參數提供, 對象引用計數爲1.
並初始化lru字段. 若服務器採用LRU算法, 則置該字段的值爲當前分鐘級別的一個時間戳. 若服務器採用LFU算法, 則置爲一個計數值.
unsigned long long estimateObjectIdleTime(robj *o) 獲取一個對象未被訪問的時間, 單位爲毫秒.
因爲redisObjectlru字段有24位, 並非無限長, 因此有循環溢出的風險, 當發生循環溢出時(即當前LRU時鐘計數比對象中的lru字段小), 那麼該函數始終保守認爲循環溢出只發生了一次

3.1 字符串對象

字符串對象支持三種編碼方式: INTRAWEMBSTR, 三種方式的內存佈局分別以下:

stringObject

字符串對象的相關接口以下:

分類 API名 功能
建立接口 robj *createEmbeddedStringObject(const char *ptr,size_t len) 建立一個編碼爲EMBSTR的字符串對象.
即底層使用SDS, 且SDS與RedisObject位於同一塊連續內存上
-- robj *createRawStringObject(const char *ptr,size_t len) 建立一個編碼爲RAW的字符串對象.
即底層使用SDS, 且SDS由RedisObject間接持有
內部是先用入參建立一個SDS, 而後用這個SDS再去調用createObject
-- robj *createStringObject(const char *ptr,size_t len) 建立一個字符串對象.
len參數的值小於或等於OBJ_ENCODING_EMBSTR_SIZE_LIMIT時, 編碼方式爲EMBSTR, 不然爲RAW
內部是經過調用createRawStringObjectcreateEmbeddedStringObject來建立不一樣編碼的字符串對象的
-- robj *createStringObjectFromLongLong(long long value) 根據整數值, 建立一個字符串對象.
若可複用全局共享字符串對象池中的對象, 則會盡可能複用. 不然以最節省內存的原則, 來決定對象的編碼
-- robj *createStringObjectFromLongDouble(long double value,int humanfriendly) 根據浮點數值, 建立一個字符串對象
其中參數humanfriendly不爲0, 則字符串以小數形式表達. 不然以exp計數法表達.根據字符串表達的長短, 編碼多是RAW, 或EMBSTR
釋放接口 void freeStringObject(robj *o) 釋放字符串對象.
若字符串對象底層使用SDS, 則調用sdsfree釋放這個SDS.
不然什麼也不作
讀寫接口 robj *dupStringObject(const robj *o) 建立一個字符串對象的深拷貝副本. 不影響原字符串對象的引用計數.
建立的副本與原字符串毫無關聯
-- int isSdsRepresentableAsLongLong(sds s,long long *llval) 判斷SDS字符串是不是一個取值在long long數值範圍內的數值的字符串表達. 若是是, 就把相應的數值置在出參中
內部調用的是string2ll來判斷

嚴格來說這不該該算是RedisObject的接口函數, 而應當算是SDS的接口函數"
-- int isObjectRepresentableAsLongLong(robj *o,long long *llval) 判斷字符串對象是不是一個取值在long long數值範圍內的數值的字符串表達. 若是是, 就把相應的數值置在出參中.
-- robj *tryObjectEncoding(robj *o) 嘗試縮減這個字符串對象的內存佔用.

策略爲:
若是字符串對象表明的是一個位於long取值範圍內的數值, 則嘗試返回全局共享字符串對象池裏的等價對象. 若因爲服務器配置等緣由不成功, 則嘗試將對象編碼改成INT
若是以上都不成功, 則嘗試將對象的編碼改成EMBSTR
若以上都不成功, 則在對象的編碼爲RAW的狀態下, 至少調用sdsRemoveFreeSpace來移除掉內部SDS中, 閒置的內存空間
-- robj *getDecodedObject(robj *o) 返回字符串對象的一個淺拷貝.
在編碼爲RAWEMBSTR時, 底層數據引用計數+1, 返回一個共享句柄
在編碼爲INT時, 返回一個編碼爲RAWEMBSTR的新副本的句柄. 新舊對象之間無關
-- size_t stringObjectLen(robj *o) 返回字符串對象中的字符個數
-- int getDoubleFromObject(const robj *o,double *target) 從字符串對象中解析出數值, 兼容整數值
-- int getLongLongFromObject(robj *o,long long *target) 從字符串對象中解析出整數值, 不兼容浮點數值
-- int getLongDoubleFromObject(robj *o,long double *target) 從字符串對象中解析出數值, 兼容整數值
-- int compareStringObjects(robj *a, robj *b) 二進制比較兩個字符串對象. 如有字符串對象使用的是INT編碼, 則先會把ptr中的數值轉化爲字符串表達, 而後再去比較
-- int collateStringObjects(robj *a, robj *b) 底層調用strcoll去比較兩個字符串對象. 比較的大小結果受LC_LOCALE的影響
-- int equalStringObjects(robj *a, robj *b) 字符串判等
-- #define sdsEncodedObject(objptr) 宏, 判斷字符串對象的內部是否爲SDS實現. 即編碼爲RAWEMBSTR

3.2 哈希對象

哈希對象的底層實現有兩種, 一種是dict, 一種是ziplist. 分別對應編碼HTZIPLIST. 而以前介紹的zipmap這種結構, 雖然也是一種輕量級的字典結構, 且縱使在源代碼中有相應的編碼宏值, 但遺憾的是, 至Redis 4.0.10, 目前哈希對象的底層編碼仍然只有ziplistdict兩種

dict自沒必要說, 自己就是字典類型, 存儲鍵值對的. 用ziplist做爲底層數據結構時, 是將鍵值對以<key1><value1><key2><value2>...<keyn><valuen>這樣的形式存儲在ziplist中的. 兩種編碼內存佈局分別以下:

hashObject

上圖中不嚴謹的地方有:

  1. ziplist中每一個entry, 除了鍵與值自己的二進制數據, 還包括其它字段, 圖中沒有畫出來
  2. dict底層可能持有兩個dictht實例
  3. 沒有畫出dict的哈希衝突

須要注意的是: 當採用HT編碼, 即便用dict做爲哈希對象的底層數據結構時, 鍵與值均是以sds的形式存儲的.

哈希對象的相關接口以下:

分類 API名 功能
建立接口 robj *createHashObject(void) 建立一個空哈希對象
底層編碼使用ZIPLIST, 即底層使用ziplist
釋放接口 void freeHashObject(robj *o) 釋放哈希對象
若哈希對象底層使用的是dict, 則調用dictRelease釋放這個dict
若哈希對象底層使用的是ziplist, 則直接釋放掉這個ziplist佔用的連續內存空間
編碼轉換接口 void hashTypeConvertZiplist(robj *o, int enc) 將哈希對象的編碼從ZIPLIST轉換爲HT, 即底層實現從ziplist轉爲dict
-- void hashTypeConvert(robj *o, int enc) 轉換哈希對象的編碼.
雖然接口設計的好像能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從ZIPLIST轉向HT
-- void hashTypeTryConversion(robj *o,robj **argv,int start,int end) o是一個哈希對象. argv是其它對象的數組.(最好是字符串對象, 且爲SDS實現)
這個函數會檢查argv數組中, 從startend之間的全部對象, 若是這些對象中, 但凡是有一個對象是字符串對象, 且長度超過了用ziplist實現哈希對象時, ziplist的限長
那麼o這個哈希對象的編碼就會從ZIPLIST轉爲HT
讀寫接口 int hashTypeSet(robj *o,sds field,sds value,int flags) 向哈希對象寫入一個鍵值對. 
在底層編碼爲HT時, flag將影響插入鍵值對時的具體行爲. flag可有標誌位 HASH_SET_TAKE_VALUEHASH_SET_TAKE_FIELD, 若對應位置1, 表明鍵與值直接引用參數值. 不然表明要調用sdsdup接口拷貝鍵與值.
在底層編碼爲ZIPLIST時, 鍵與值必然會被拷貝
-- int hashTypeExists(robj *o, sds field) 查詢指定鍵在哈希對象中是否存在
-- unsigned long hashTypeLength(const robj *o) 查詢哈希對象中的鍵值對總數
-- int hashTypeGetFromZiplist(robj *o, sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) 從編碼爲ZIPLIST的哈希對象中, 取出一個鍵對應的值. 
鍵從field傳入, 當值爲數值類型時, 值以*vll傳出, 當值爲二進制類型時, 值以*vstr*vlen傳出
-- sds hashTypeGetFromHashTable(robj *o, sds field) 從編碼爲HT的哈希對象中, 取出一個鍵對應的值.
鍵從field傳入, 值以返回值傳出. 若值不存在, 返回NULL"
-- "int hashTypeGetValue(robj *o,sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) 取出哈希對象中指定鍵對應的值. 若值是數值類型, 則以*vll傳出, 不然以*vstr*vlen傳出
-- robj *hashTypeGetValueObject(robj *o, sds field) 取出哈希對象中指定鍵對應的值, 幷包裝成RedisObject返回. 返回的對象爲字符串對象
-- size_t hashTypeGetValueLength(robj *o, sds field) 取出哈希對象中指定鍵對應的值的長度
-- int hashTypeDelete(robj *o, sds field) 刪除哈希對象中的一個鍵值對. 鍵不存在時返回0, 成功刪除返回1
迭代器接口 hashTypeIterator *hashTypeInitIterator(robj *subject) 在指定哈希對象上建立一個迭代器
-- void hashTypeReleaseIterator(hashTypeIterator *hi) 釋放哈希對象的迭代器
-- int hashTypeNext(hashTypeIterator *hi) 讓哈希迭代器步進一步
-- void hashTypeCurrentFromZiplist(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) 取出哈希對象迭代器當前指向的鍵 或值. 當what傳入OBJ_HASH_KEY時, 取的是鍵, 不然取的是值.
注意, 該函數僅在哈希對象的編碼爲ZIPLIST時才能正確運行
-- sds hashTypeCurrentFromHashTable(hashTypeIterator *hi,int what) 取出哈希對象迭代器當前指向的鍵 或值. 當what傳入OBJ_HASH_KEY時, 取的是鍵, 不然取的是值.
注意, 該函數僅在哈希對象的編碼爲HT時才能正確運行
-- void hashTypeCurrentObject(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) 取出哈希對象迭代器當前指向的鍵或值. 當what傳入OBJ_HASH_KEY時, 取的是鍵, 不然取的是值.
-- sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi,int what) 取出哈希對象迭代器當前指向的鍵或值. 且把鍵或值以一個全新的SDS字符串返回. 當what傳入OBJ_HASH_KEY時, 取的是鍵, 不然取的是值.

3.3 列表對象

列表對象的底層實現, 歷史上是有兩種的, 分別是ziplistlist, 但截止Redis 4.0.10版本, 全部的列表對象API都再也不支持除去quicklist以外的任何底層實現. 也就是說, 目前(Redis 4.0.10), 列表對象支持的底層實現實質上只有一種, 便是quicklist.

列表對象的建立API依然支持從ziplist的實例建立一個列表對象, 即你能夠建立一個底層編碼爲ZIPLIST的列表對象, 但若是用該列表對象去調用任何其它列表對象的API, 都會致使panic. 在使用以前, 你只能再次調用相關的底層編碼轉換接口, 將這個列表對象的底層編碼轉換爲QUICKLIST.

而且遺憾的是, LINKEDLIST這種編碼, 即底層爲list的列表, 被完全淘汰了. 也就是說, 截止目前(Redis 4.0.10), Redis定義的10個對象編碼方式宏名中, 有兩個被徹底閒置了, 分別是: OBJ_ENCODING_ZIPMAPOBJ_ENCODING_LINKEDLIST. 從Redis的演進歷史上來看, 前者是後續可能會獲得支持的編碼值, 後者則應該是被完全淘汰了.

列表對象的內存佈局以下圖所示:

listObject

列表對象的API接口以下:

分類 API名 功能
建立接口 robj *createQuicklistObject(void) 建立一個列表對象. 內部編碼爲QUICKLIST
即內部使用quicklist實現的列表對象
-- robj *createZiplistObject(void) 建立一個列表對象. 內部編碼爲ZIPLIST
即內部使用ziplist實現的列表對象
釋放接口 void freeListObject(robj *o) 釋放一個列表對象
編碼轉換接口 void listTypeConvert(robj *subject, int enc) 轉換列表對象的內部編碼.
雖然接口設計的好你能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從ZIPLIST轉換爲QUICKLIST
而且蛋疼的是, 4.0.10這個版本中, 全部的列表對象操做API內部實現都僅支持編碼方式爲QUICKLIST的列表對象, 其它編碼方式會panic.
因此目前爲止, 這個API的惟一做用, 就是配合createZiplistObject接口, 來使用一個ziplist建立一個內部編碼爲QUICKLIST的列表對象.
讀寫接口 void listTypePush(robj *subject,robj *value,int where) 向列表對象中添加一個數據.
where參數的值控制是在頭部添加, 仍是尾部添加.
where可選的值爲LIST_HEADLIST_TAIL
-- robj *listTypePop(robj *subject,int where) 從列表對象的頭部或尾部取出一個數據.
取出的數據經過被包裝成字符串對象後返回. 具體取出位置經過參數where控制
-- unsigned long listTypeLength(const robj *subject) 獲取列表對象中保存的數據的個數
-- void listTypeInsert(listTypeEntry *entry,robj *value, int where) 將字符串對象中的數據插入到列表對象的頭部或尾部.
插入過程當中不會拷貝字符串對象持有的數據自己. 但會縮減字符串對象的引用計數.
-- int listTypeEqual(listTypeEntry *entry, robj *o) 判斷字符串對象o與列表對象中指定位置上存儲的數據是否相同.
-- robj *listTypeGet(listTypeEntry *entry) 獲取列表對象中指定位置的數據.
位置信息經過entry傳入, 這是一個入參. 數據將拷貝一份後經過SDS形式返回
迭代器接口 listTypeIterator *listTypeInitIterator(robj *subject,long index,unsigned char direction) 建立一個列表對象迭代器
-- void listTypeReleaseIterator(listTypeIterator *li) 釋放一個列表對象迭代器
-- int listTypeNext( listTypeIterator *li, listTypeEntry *entry) 讓列表對象迭代器步進一步, 並將步進以前迭代器所指向的數據保存在entry
-- void listTypeDelete( listTypeIterator *iter, listTypeEntry *entry) 刪除列表迭代器當前指向的列表對象中存儲的數據.
被刪除的數據經過entry返回

3.4 集合對象

集合對象的底層實現有兩種, 分別是intsetdict. 分別對應編碼宏中的INTSETHT. 顯然當使用intset做爲底層實現的數據結構時, 集合中存儲的只能是數值數據, 且必須是整數. 而當使用dict做爲集合對象的底層實現時, 是將數據所有存儲於dict的鍵中, 值字段閒置不用.

集合對象的內存佈局以下圖所示:

setObject

集合對象的API接口以下:

分類 API名 功能
建立接口 robj *createSetObject(void) 建立一個空集合對象.
底層編碼使用HT, 即底層使用dict
-- robj *createIntsetObject(void) 建立一個空集合對象.
底層編碼使用INTSET, 即底層使用intset
-- robj *setTypeCreate(sds value) 建立一個空集合對象.
注意入參雖然攜帶了一個數據, 但這個數據並不會存儲在集合中
這個數據只起到決定編碼方式的做用, 若這個數據是數值的字符串表達, 則底層編碼則爲INTSET, 不然爲HT
釋放接口 void freeSetObject(robj *o) 釋放集合對象.
若集合對象底層使用的是dict, 則調用dictRelease釋放這個dict
若集合對象底層使用的是intset, 則直接釋放這個intset佔用的連續內存
編碼轉換接口 void setTypeConvert(robj *setobj, int enc) 轉換集合對象的內部編碼
雖然接口設計的好你能夠在底層編碼之間互相轉換, 但實際上這個接口的實現, 目前僅支持從INTSET轉換爲HT
讀寫接口 int setTypeAdd(robj *subject, sds value) 向集合對象中寫入一個數據
-- int setTypeRemove(robj *setobj, sds value) 刪除集合對象中的一個數據
-- int setTypeIsMember(robj *subject, sds value) 判斷指定數據是否在集合對象中
-- int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele) 從集合對象中, 隨機選出一個數據, 將其數據經過出參返回.
若數據是數值類型, 則從*llele返回, 不然, 從*sdsele返回.
注意該接口若取得二進制數據, 則*sdsele是直接引用集合內的數據, 而不是拷貝一份
-- unsigned long setTypeSize(const robj *subject) 返回集合中數據的個數
迭代器接口 setTypeIterator *setTypeInitIterator(robj *subject) 建立一個集合對象迭代器
-- void setTypeReleaseIterator(setTypeIterator *si) 釋放集合對象迭代器
-- int setTypeNext( setTypeIterator *si, sds *sdsele, int64_t *llele) 讓集合迭代器步進一步, 並從出參中返回步進前迭代器所指向的數據.
若數據是數值類型, 則從*llele返回, 不然, 從*sdsele返回
注意該接口若取得二進制數據, 則*sdsele是直接引用集合內的數據, 而不是拷貝一份
-- sds setTypeNextObject(setTypeIterator *si) 讓集合迭代器步進一步, 並把步進前所指向的數據, 拷貝一份, 構形成一個新的SDS, 做爲返回值返回

3.5 有序集合對象

有序集合的底層實現依然有兩種, 一種是使用ziplist做爲底層實現, 另一種比較特殊, 底層使用了兩種數據結構: dictskiplist. 前者對應的編碼值宏爲ZIPLIST, 後者對應的編碼值宏爲SKIPLIST

使用ziplist來實如今序集合很容易理解, 只須要在ziplist這個數據結構的基礎上作好排序與去重就能夠了. 使用zskiplist來實現有序集合也很容易理解, Redis中實現的這個跳躍表彷佛自然就是爲了實現有序集合對象而實現的, 那麼爲何還要輔助一個dict實例呢? 咱們先看來有序集合對象在這兩種編碼方式下的內存佈局, 而後再作解釋:

首先是編碼爲ZIPLIST時, 有序集合的內存佈局以下:

zsetObject_ZIPLIST

而後是編碼爲SKIPLIST時, 有序集合的內存佈局以下:

zsetObject_SKIPLIST

在使用dictskiplist實現有序集合時, 跳躍表負責按分數索引, 字典負責按數據索引. 跳躍表按分數來索引, 查找時間複雜度爲O(lgn). 字典按數據索引時, 查找時間複雜度爲O(1). 設想若是沒有字典, 若是想按數據查分數, 就必須進行遍歷. 兩套底層數據結構均只做爲索引使用, 即不直接持有數據自己. 數據被封裝在SDS中, 由跳躍表與字典共同持有. 而數據的分數則由跳躍表結點直接持有(double類型數據), 由字典間接持有.

有序集合對象的API接口以下:

分類 API名 功能
建立接口 robj *createZsetObject(void) 建立一個有序集合對象
默認內部編碼爲SKIPLIST, 即內部使用zskiplist與dict來實現有序集合
-- robj *createZsetZiplistObject(void) 建立一個有序集合對象
指定內部編碼爲ZIPLIST, 即內部使用ziplist來實現有序集合
釋放接口 void freeZsetObject(robj *o) 釋放一個有序集合對象
編碼轉換接口 void zsetConvert(robj *zobj, int encoding) 轉換有序集合對象的內部編碼
能夠在ZIPLISTSKIPLIST兩種編碼間轉換
-- void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen) 判斷當前有序集合對象是否有必要將底層編碼轉換爲ZIPLIST, 若是有必要, 就執行轉換
讀寫接口 int zsetScore(robj *zobj, sds member, double *score) 獲取有序集合中, 指定數據的得分.
數據由member參數攜帶, 經過二進制判等的方式匹配
-- int zsetAdd( robj *zobj, double score, sds ele, int *flags, double *newscore) 向有序集合中添加數據, 或更新已存在的數據的得分.
flag是一個in-out參數, 其做爲入參, 控制函數的具體行爲, 其做爲出參, 報告函數執行的結果.

做爲入參時, *flags的語義以下:
ZADD_INCR 遞增已存在的數據的得分. 若是數據不存在, 則添加數據, 並設置得分. 且若newscore != NULL, 執行操做後, 數據的得分還會賦值給*newscore
ZADD_NX 僅當數據不存在時, 執行添加數據並設置得分, 不然什麼也不作
ZADD_XX 僅當數據存在時, 執行重置數據得分. 不然什麼也不作

做爲出參, *flags的語義以下:
ZADD_NAN 數據的得分不是一個數值, 表明內部出現的異常
ZADD_ADDED 新數據已經添加至集合中
ZADD_UPDATED 數據的得分已經更新
ZADD_NOP 函數什麼也沒作
-- int zsetDel(robj *zobj, sds ele) 從有序集合中移除一個數據
-- long zsetRank(robj *zobj, sds ele, int reverse) 獲取有序集合中, 指定數據的排名.
reverse==0, 排名以得分升序排列. 不然排名以得分降序排列.
第一個數據的排名爲0, 而不是1
-- unsigned int zsetLength(const robj *zobj) 獲取有序集合對象中存儲的數據個數
相關文章
相關標籤/搜索