Redis源碼剖析 有序集合對象t_zset實現

有序集合對象其實跟集合對象類似,只不過它多了一個score的參數,集合中的每個元素都有一個分值,在集合中元素是按照score排序的。

有序集合的底層編碼也是有兩種實現,壓縮列表REDIS_ENCODING_ZIPLIST以及跳躍表REDIS_ENCODING_SKIPLIST。和集合的一樣,有序集合的編碼方式是通過檢查第一個被加入的元素來決定的。

ZSET結構

/* RedisObject結構 */
typedef struct redisObject {
    unsigned type:4;  // OBJ_ZSET表示有序集合對象
    unsigned encoding:4;  // 編碼字段爲OBJ_ENCODING_ZIPLIST或OBJ_ENCODING_SKIPLIST
    unsigned lru:LRU_BITS; // LRU_BITS爲24位
    int refcount;
    void *ptr;  // 指向數據部分
} robj;

同其他的對象一樣, zset結構也是存儲在redisObject結構體中,通過指定 type= OBJ_ZSET 來確定這是一個有序集合對象,當是一個有序集合對象的時候,配套的endoding只能有對應的兩種取值。

ZIPLIST編碼的有序集合

當用REDIS_ENCODING_ZIPLIST作爲有序集合的底層編碼時,有序集合中的元素按score值從小到大排序,先保存value,在保存score,示意圖如下

          |<--  element 1 -->|<--  element 2 -->|<--   .......   -->|

+---------+---------+--------+---------+--------+---------+---------+---------+
| ZIPLIST |         |        |         |        |         |         | ZIPLIST |
| ENTRY   | member1 | score1 | member2 | score2 |   ...   |   ...   | ENTRY   |
| HEAD    |         |        |         |        |         |         | END     |
+---------+---------+--------+---------+--------+---------+---------+---------+

SKIPLIST編碼的有序集合

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

當有序集合用REDIS_ENCODING_SKIPLIST編碼的時候,並不僅僅是用了跳躍表skiplist,還用到了字典dict。爲什麼要這麼做呢。 跳躍表的優點是能夠在O(log_N_)的期望時間內根據分值score對元素進行定位,對於一些範圍查找命令,比如ZRANGE能夠較好的支持。

但是如果要取出對應元素的分值,或者查看沒有某個元素是否在有序集合內,單純靠跳躍表就不夠用了, 因爲跳躍表是根據score組織的,不是根據元素值組織的。所以在有序集合中另外用了一個dict來支持這些操作。dict中key就是元素的值,value就是score。這樣就能夠在O(1)的複雜度內找到對應的分值,或者判斷一個元素是否在有序集合中。

ZSET編碼轉換

如果一個有序集合一開始是用壓縮列表REDIS_ENCODING_ZIPLIST作爲底層編碼,只要滿足下邊的條件,就會將底層編碼轉換爲REDIS_ENCODING_SKIPLIST:

  • ziplist 所保存的元素數量超過服務器屬性 server.zset_max_ziplist_entries 的值(默認值爲 128 )
  • 新添加元素的長度大於服務器屬性 server.zset_max_ziplist_value 的值(默認值爲 64 )
void zsetConvert(robj *zobj, int encoding) {
    zset *zs;
    zskiplistNode *node, *next;
    sds ele;
    double score;
    // 如果已經是目標編碼格式,返回
    if (zobj->encoding == encoding) return;
    // 編碼格式從ZIPLIST轉成SKIPLIST
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *zl = zobj->ptr;
        unsigned char *eptr, *sptr;
        unsigned char *vstr;
        unsigned int vlen;
        long long vlong;
        // 如果目標編碼格式不對,返回錯誤
        if (encoding != OBJ_ENCODING_SKIPLIST)
            serverPanic("Unknown target encoding");
        // 創建新的zset,底層爲SKIPLIST編碼,所以需要兩個結構體:一個dict和一個skiplist
        zs = zmalloc(sizeof(*zs));
        zs->dict = dictCreate(&zsetDictType,NULL);
        zs->zsl = zslCreate();

        eptr = ziplistIndex(zl,0);
        serverAssertWithInfo(NULL,zobj,eptr != NULL);
        sptr = ziplistNext(zl,eptr);
        serverAssertWithInfo(NULL,zobj,sptr != NULL);
        // 循環遍歷ZIPLIST
        while (eptr != NULL) {
            // 獲取score
            score = zzlGetScore(sptr);
            serverAssertWithInfo(NULL,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
            // 獲得對應的元素的值
            if (vstr == NULL)
                ele = sdsfromlonglong(vlong);
            else
                ele = sdsnewlen((char*)vstr,vlen);
            // 根據元素值和score新建node,並插入到dict中
            node = zslInsert(zs->zsl,score,ele);
            serverAssert(dictAdd(zs->dict,ele,&node->score) == DICT_OK);
            zzlNext(zl,&eptr,&sptr);
        }
        // 將zobj指向新的zset
        zfree(zobj->ptr);
        zobj->ptr = zs;
        zobj->encoding = OBJ_ENCODING_SKIPLIST;
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        // 編碼格式從SKIPLIST轉換成ZIPLIST
        unsigned char *zl = ziplistNew();
        // 檢查目標編碼格式,錯誤退出
        if (encoding != OBJ_ENCODING_ZIPLIST)
            serverPanic("Unknown target encoding");

        /* Approach similar to zslFree(), since we want to free the skiplist at
         * the same time as creating the ziplist. */
        zs = zobj->ptr;
        // 釋放dict的空間
        dictRelease(zs->dict);
        // skiplist的頭節點空間
        node = zs->zsl->header->level[0].forward;
        // 釋放表頭
        zfree(zs->zsl->header);
        zfree(zs->zsl);
        // 遍歷跳躍表
        while (node) {
            // 將元素ele和score添加到ziplist中
            zl = zzlInsertAt(zl,NULL,node->ele,node->score);
            next = node->level[0].forward;
            zslFreeNode(node);
            node = next;
        }
        // 釋放zs,並集鞥zobj指向新的zl
        zfree(zs);
        zobj->ptr = zl;
        zobj->encoding = OBJ_ENCODING_ZIPLIST;
    } else {
        serverPanic("Unknown sorted set encoding");
    }
}

ZSET命令

命令 說明
ZADD key score member [[score member] [score member] …] 將一個或多個 member 元素及其 score 值加入到有序集 key 當中
zcard 返回有序集 key 的基數
ZCOUNT key min max 返回有序集 key 中, score 值在 min 和 max 之間(默認包括 score 值等於 min 或 max )的成員的數量
ZINCRBY key increment member 爲有序集 key 的成員 member 的 score 值加上增量 increment
zrange 返回有序集 key 中,指定區間內的成員
zrevrange 返回有序集 key 中,指定區間內的成員
zrangeByScore 返回有序集 key 中,所有 score 值介於 min 和 max 之間(包括等於 min 或 max )的成員
zrank 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞增(從小到大)順序排列
zrevrank 返回有序集 key 中成員 member 的排名。其中有序集成員按 score 值遞減(從大到小)排序
zrem 移除有序集 key 中的一個或多個成員,不存在的成員將被忽略
zscore 返回有序集 key 中,成員 member 的 score 值

ZSET命令實現

ZADD接口實現

區別於其他有多種底層編碼格式的實現(比如集合SET),有序集合不是在一個函數中區別不同的底層編碼來實現功能,而是分別搞了兩套機制,比如ZADD命令,有一個壓縮列表編碼的 zzlInsert 函數以及跳躍表編碼的 zslInsert 函數。在命令一進來的時候,就根據不同的編碼格式,調用不同的函數實現。

ZIPLIST的中插入接口

/* Insert (element,score) pair in ziplist. This function assumes the element is
 * not yet present in the list. */
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {
    unsigned char *eptr = ziplistIndex(zl,0), *sptr;
    double s;
    // 循環遍歷ZIPLIST
    while (eptr != NULL) {
        sptr = ziplistNext(zl,eptr);
        serverAssert(sptr != NULL);
        // 獲取分值
        s = zzlGetScore(sptr);
        // 如果分值大於score,說明已經找到了要插入的位置
        if (s > score) {
            /* First element with score larger than score for element to be
             * inserted. This means we should take its spot in the list to
             * maintain ordering. */
            // 在對應位置插入元素和score
            zl = zzlInsertAt(zl,eptr,ele,score);
            break;
        } else if (s == score) {
            /* Ensure lexicographical ordering for elements. */
            // 如果分值相同,按字典序排列
            if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {
                zl = zzlInsertAt(zl,eptr,ele,score);
                break;
            }
        }

        /* Move to next element. */
        eptr = ziplistNext(zl,sptr);
    }

    /* Push on tail of list when it was not yet inserted. */
    // 如果到了最後,說明前邊的score都比目標要小,直接在尾部插入
    if (eptr == NULL)
        zl = zzlInsertAt(zl,NULL,ele,score);
    return zl;
}

SKIPLIST的插入接口在跳躍表那節介紹過,重新貼一下

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
 // 跳躍表插入元素
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));    // 判斷是否爲數字
    x = zsl->header;
    // 從最高的level, 也即跨度最大的level開始查找結點
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        // 當前是否是最高層, 如果是最高層,rank[i]=0,否則,複製上一層的數值
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 如果當前結點的score值小於傳入的score 或者 當前score相等,但是結點的對象不相等
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            // 將當前一層的跨度加到rank[i]
            rank[i] += x->level[i].span;
            // 在當前層中向前查找
            x = x->level[i].forward;
        }
        // 當前層位於插入位置前的結點x放入update數組
        update[i] = x;
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    // 隨機生成小於32的層數
    level = zslRandomLevel();
    // 如果生成的層數大於當前的層數
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            // 設定rank數組中大於原level層以上的值爲0
            // 同時設定update數組大於原level層以上的數據
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    // 創建層數爲level的新結點
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        // 將每一層的前置結點的後續結點指向新結點, 同時設置新結點的後續結點
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        /* update span covered by update[i] as x is inserted here */
        // 更新每一層的前置結點和新結點的跨度
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    // 根據最低層的前序結點是否是header結點來設置當前新結點的向後指針
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章