redis 基礎數據結構 之 有序集合

給新觀衆老爺的開場

大家好,我是弟弟!
最近讀了一遍 黃健宏大佬的 <<Redis 設計與實現>>,對Redis 3.0版本有了一些認識
該書作者有一版添加了註釋的 redis 3.0源碼
👉官方redis的github傳送門
👉黃健宏大佬添加了註釋的 redis 3.0源碼傳送門

網上說Redis代碼寫得很好,爲了加深印象和學習redis大佬的代碼寫作藝術,瞭解工作中使用的redis 命令背後的源碼邏輯,便有了寫博客記錄學習redis源碼過程的想法。

redis 有序集合(zset)

redis的有序集合 跟 集合一樣成員是唯一的,跟集合不同的是每個成員都會關聯一個分數用來排序,分數可以重複。

redis有序集合的底層實現數據結構 有兩種,分別是 ziplist和zset。

這取決於有序集合中的成員數量或者 單個成員的key的長度

當有序集合成員數超過 zset_max_ziplist_entries(默認128),
或者成員的key的長度超過 zset_max_ziplist_value(默認64)時,
會將有序集合的實現方式從ziplist轉化爲zset。
在其他一些地方, 也可能從zset轉化成ziplist。

redis有序集合 第一種實現方式 ziplist

ziplist實現的有序集合,會將成員的key和分數拆成兩個緊挨着的ziplist元素,key在前,分數災後。
有序集合成員按 成員分數從小到大排列,
相同分數的有序集合成員,按照成員key的字典序從小到大排列。

ziplist的詳細內容可以參考往期博客 👇
往期博客 redis源碼閱讀 - 基礎數據結構 之 ziplist

redis有序集合 第二種實現方式 zset

zset實現的有序集合,包含了一個 k/v字典 和 一個 跳躍表

zset結構一覽 👇

 * 有序集合
typedef struct zset
{
    // 字典,鍵爲成員,值爲分值
    // 用於支持 O(1) 複雜度的按成員取分值操作
    dict *dict;
    // 跳躍表,按分值排序成員
    // 用於支持平均複雜度爲 O(log N) 的按分值定位成員操作
    // 以及範圍操作
    zskiplist *zsl;
} zset;

哈希表的詳細結構可參考往期博客
redis 基礎數據結構之 hash表
redis不穩定字典的遍歷

跳躍表

跳躍表可以簡單理解爲 是一個用鏈表實現的
使用二分查找思想來加速查詢,具有區間遍歷功能的數據結構

跳躍表裏有一個雙向鏈表,便於雙向遍歷。每個鏈表元素記錄了成員的key和分數
並且鏈表元素的順序是按分值從小到大排列,相同分值按key的字典序從小到大排列。

在每一個鏈表節點上,有一個層的概念,
在最大層數以下,每一層有兩個字段,
一個是該層的跨度,也就是在該層從當前節點n1,到節點n2 中間跨越了幾個元素
層越高,跨度越大
另一個就是指向節點n2的指針,叫當前層的前進指針
每個鏈表節點還有一個指向前一個鏈表的指針,叫後退指針

這樣不管是計算鏈表節點的排名,還是做區間遍歷,都能快速定位到需要查找的節點

因爲層越高跨度越大,從高層開始遍歷查找是,能經過很少的比較次數,就能快速定位到所要查找節點,在該層所在的區間,然後再一層一層遍歷下去,直到找到

跳躍表結構 與跳躍表節點結構一覽

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


/* ZSETs use a specialized version of Skiplists */
/*
 * 跳躍表節點
 */
typedef struct zskiplistNode
{
    // 成員對象
    robj *obj;
    // 分值
    double score;
    // 後退指針,指向前一個跳躍表節點
    struct zskiplistNode *backward;
    // 層
    struct zskiplistLevel
    {
        // 前進指針,指向跨越span個元素後的跳躍表節點
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

在跳躍表上查找元素

在zset中單純的查找key的跳躍表節點,是通過字典,也就是hash表的查找來完成的
當在跳躍表上插入一個跳躍表節點,或者取跳躍表節點的排名等操作時,需要在跳躍表上進行。

查找單個元素,hash表的查詢效率一般是o(1)的,而跳躍表一般是o(logN)

ZRANK 在跳躍表上取節點的排名

zrank跳躍表相關的處理邏輯大致爲

  1. 通過有序集合的key,找到有序集合(僅討論zset)
  2. 通過成員名在zset的字典中查找,對應的key是否存在
  3. 存在,則返回字典中的value,也就是成員的分數
  4. 通過成員key和成員分數,遍歷跳躍表計算出排名

skiplist計算成員排名的邏輯大致爲

  1. 從跳躍表的頭節點的最高層開始遍歷

  2. 在每一層中,通過成員分數以及成員的key,來查找所在的區間

  3. 查找區間的方法爲,
    當前層的前進指針指向的節點的分數

    3.1
    若小於被查找成員分數,若分數相等,且key小於被查找成員的key
    則在當前層中向前遍歷,累加當前節點的跨度記錄到rank變量中,
    並將指針設爲前進指針

    3.2
    否則,被查找成員落在該區域,層數減一,繼續遍歷

  4. 結束條件
    4.1 找到被查找成員,返回rank值
    4.2 層高小於0,沒查到,返回0

    skiplist的rank值是從1開始的,因爲有一個表頭節點,詳細在後面sadd相關的代碼邏輯裏能看到

讓我們來看下zrank取排名的源碼邏輯,zrank命令對應的處理函數是

void zrankCommand(redisClient *c) {
    zrankGenericCommand(c, 0);
}

void zrankGenericCommand(redisClient *c, int reverse) {
    robj *key = c->argv[1];
    robj *ele = c->argv[2];
    robj *zobj;
    unsigned long llen;
    unsigned long rank;

    // 有序集合
    if ((zobj = lookupKeyReadOrReply(c,key,shared.nullbulk)) == NULL ||
        checkType(c,zobj,REDIS_ZSET)) return;
    // 元素數量
    llen = zsetLength(zobj);
	...
    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {//壓縮列表
        ...
    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {//跳躍表
        zset *zs = zobj->ptr;
        zskiplist *zsl = zs->zsl;
        dictEntry *de;
        double score;

        // 從字典中取出元素
        ele = c->argv[2] = tryObjectEncoding(c->argv[2]);
        de = dictFind(zs->dict,ele);
        if (de != NULL) {

            // 取出元素的分值
            score = *(double*)dictGetVal(de);

            // 在跳躍表中計算該元素的排位
            rank = zslGetRank(zsl,score,ele);
            redisAssertWithInfo(c,ele,rank); /* Existing elements always have a rank. */

            // ZRANK 還是 ZREVRANK ?
            if (reverse)
                addReplyLongLong(c,llen-rank);
            else
                addReplyLongLong(c,rank-1);
        } else {
            addReply(c,shared.nullbulk);
        }

    } else {
        redisPanic("Unknown sorted set encoding");
    }
}

unsigned int zsetLength(robj *zobj) {

    int length = -1;

    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
        length = zzlLength(zobj->ptr);

    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
        length = ((zset*)zobj->ptr)->zsl->length;

    } else {
        redisPanic("Unknown sorted set encoding");
    }

    return length;
}

/* Find the rank for an element by both score and key.
 *
 * 查找包含給定分值和成員對象的節點在跳躍表中的排位。
 *
 * Returns 0 when the element cannot be found, rank otherwise.
 *
 * 如果沒有包含給定分值和成員對象的節點,返回 0 ,否則返回排位。
 *
 * Note that the rank is 1-based due to the span of zsl->header to the
 * first element. 
 *
 * 注意,因爲跳躍表的表頭也被計算在內,所以返回的排位以 1 爲起始值。
 *
 * T_wrost = O(N), T_avg = O(log N)
 */
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;

    // 遍歷整個跳躍表
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {

        // 遍歷節點並對比元素
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                // 比對分值
                (x->level[i].forward->score == score &&
                // 比對成員對象
                compareStringObjects(x->level[i].forward->obj,o) <= 0))) {

            // 累積跨越的節點數量
            rank += x->level[i].span;

            // 沿着前進指針遍歷跳躍表
            x = x->level[i].forward;
        }

        /* x might be equal to zsl->header, so test if obj is non-NULL */
        // 必須確保不僅分值相等,而且成員對象也要相等
        // T = O(N)
        if (x->obj && equalStringObjects(x->obj,o)) {
            return rank;
        }
    }

    // 沒找到
    return 0;
}

ZADD 在跳躍表中加入一個成員(key+分數)

在跳躍表中插入一個成員的邏輯大概如下

  1. 首先要找到在何處插入成員,查找方法跟zrank查找的方法一樣

  2. 該成員從最高層 maxlevel 到最底層 minlevel,每一層都存在一個該分數對應的區間,

    這點沒有問題吧,可以想想爲什麼🙃️

    遍歷過程中會記錄 相應區間左邊的那個節點 記在update[level]中,
    因爲後續會用來更新部分受影響的跳躍表的區間

  3. 在該成員被插入後,會給該成員隨機產生一個層數 newlevel,

    若newlevel > maxlevel
    將跳躍表表頭節點的層數從 maxlevel 升到 newlevel ,
    並且將 maxlevel 升到 newlevel 的每一層的 span設置爲當前跳躍表元素的個數
    並記錄每層需要被更新的節點 update[level] = zsl->header 跳躍表表頭節點

    newlevel的隨機生成機制,讓越大的層數產生的機率越小。
    這樣讓跳躍表在高層具有較大的區間跨度,從高層往底層,區間跨度相對越來越小,用來加速查找。

  4. 從最底層minlevel,到newlevel,將每一層的update[level]修正
    4.1
    每層包含了新插入節點的區間會被一分爲二
    也就是 update[level] 到 update[level].forward 這一個區間會被一份爲二

    4.2
    並且會修正 minlevel 到 newlevel每層update[level]節點的span值,因爲區間被一分爲二,span值可能會受影響。
    若newlevel小於maxlevel,將newlevel到maxlevel的update[level]的span值+1

    4.3
    並且會修正minlevel 到 newlevel 每層update[level], update[level].forward,以及新節點的 前/後指針

    如果沒有這些騷操作,往跳躍表中插入了一個成員會怎樣?
    🙃️ 那不就成裸的鏈表了嗎

來看下在跳躍表中插入一個成員的源碼邏輯

/*
 * 創建一個成員爲 obj ,分值爲 score 的新節點,
 * 並將這個新節點插入到跳躍表 zsl 中。
 * 
 * 函數的返回值爲新節點。
 *
 * T_wrost = O(N^2), T_avg = O(N log N)
 */
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    redisAssert(!isnan(score));

    // 在各個層查找節點的插入位置
    // T_wrost = O(N^2), T_avg = O(N log N)
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {

        /* store rank that is crossed to reach the insert position */
        // 如果 i 不是 zsl->level-1 層
        // 那麼 i 層的起始 rank 值爲 i+1 層的 rank 值
        // 各個層的 rank 值一層層累積
        // 最終 rank[0] 的值加一就是新節點的前置節點的排位
        // rank[0] 會在後面成爲計算 span 值和 rank 值的基礎
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];

        // 沿着前進指針遍歷跳躍表
        // T_wrost = O(N^2), T_avg = O(N log N)
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                // 比對分值
                (x->level[i].forward->score == score &&
                // 比對成員, T = O(N)
                compareStringObjects(x->level[i].forward->obj,obj) < 0))) {

            // 記錄沿途跨越了多少個節點
            rank[i] += x->level[i].span;

            // 移動至下一指針
            x = x->level[i].forward;
        }
        // 記錄將要和新節點相連接的節點
        update[i] = x;
    }

    /* we assume the key is not already inside, since we allow duplicated
     * scores, and the re-insertion of score and redis object should never
     * happen since the caller of zslInsert() should test in the hash table
     * if the element is already inside or not. 
     *
     * zslInsert() 的調用者會確保同分值且同成員的元素不會出現,
     * 所以這裏不需要進一步進行檢查,可以直接創建新元素。
     */

    // 獲取一個隨機值作爲新節點的層數
    // T = O(N)
    level = zslRandomLevel();

    // 如果新節點的層數比表中其他節點的層數都要大
    // 那麼初始化表頭節點中未使用的層,並將它們記錄到 update 數組中
    // 將來也指向新節點
    if (level > zsl->level) {

        // 初始化未使用層
        // T = O(1)
        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,obj);

    // 將前面記錄的指針指向新節點,並做相應的設置
    // T = O(1)
    for (i = 0; i < level; i++) {
        
        // 設置新節點的 forward 指針
        x->level[i].forward = update[i]->level[i].forward;
        
        // 將沿途記錄的各個節點的 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]);

        // 更新新節點插入之後,沿途節點的 span 值
        // 其中的 +1 計算的是新節點
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    // 未接觸的節點的 span 值也需要增一,這些節點直接從表頭指向新節點
    // T = O(1)
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }

    // 設置新節點的後退指針
    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;
}

/* Returns a random level for the new skiplist node we are going to create.
 *
 * 返回一個隨機值,用作新跳躍表節點的層數。
 *
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. 
 *
 * 返回值介乎 1 和 ZSKIPLIST_MAXLEVEL 之間(包含 ZSKIPLIST_MAXLEVEL),
 * 根據隨機算法所使用的冪次定律,越大的值生成的機率越小。
 *
 * T = O(N)
 */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
int zslRandomLevel(void) {
    int level = 1;

    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;

    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}


/*
 * 創建一個層數爲 level 的跳躍表節點,
 * 並將節點的成員對象設置爲 obj ,分值設置爲 score 。
 *
 * 返回值爲新創建的跳躍表節點
 *
 * T = O(1)
 */
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    
    // 分配空間
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));

    // 設置屬性
    zn->score = score;
    zn->obj = obj;

    return zn;
}

到這裏我們就瞭解到了在跳躍表中取排名,已經插入成員的源碼邏輯。

zadd命令就是對這些功能的包裝函數
若zadd添加的是一個不存在的成員,會在跳躍表中插入該成員節點
若是已存在的節點,會先刪除該成員節點,再重新插入該成員節點。
刪除成員節點的邏輯,差不多就是插入的邏輯的逆過程

來讓我們看下zadd與跳躍表相關的源碼邏輯👇

void zaddCommand(redisClient *c) {
    zaddGenericCommand(c,0);
}

/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(redisClient *c, int incr) {

    static char *nanerr = "resulting score is not a number (NaN)";

    robj *key = c->argv[1];
    robj *ele;
    robj *zobj;
    robj *curobj;
    double score = 0, *scores = NULL, curscore = 0.0;
    int j, elements = (c->argc-2)/2;
    int added = 0, updated = 0;

    // 輸入的 score - member 參數必須是成對出現的
    if (c->argc % 2) {
        addReply(c,shared.syntaxerr);
        return;
    }

    /* Start parsing all the scores, we need to emit any syntax error
     * before executing additions to the sorted set, as the command should
     * either execute fully or nothing at all. */
    // 取出所有輸入的 score 分值
    scores = zmalloc(sizeof(double)*elements);
    for (j = 0; j < elements; j++) {
        if (getDoubleFromObjectOrReply(c,c->argv[2+j*2],&scores[j],NULL)
            != REDIS_OK) goto cleanup;
    }

    /* Lookup the key and create the sorted set if does not exist. */
    // 取出有序集合對象
    zobj = lookupKeyWrite(c->db,key);
    if (zobj == NULL) {
        // 有序集合不存在,創建新有序集合
        if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[3]->ptr))
        {
            zobj = createZsetObject();
        } else {
            zobj = createZsetZiplistObject();
        }
        // 關聯對象到數據庫
        dbAdd(c->db,key,zobj);
    } else {
        // 對象存在,檢查類型
        if (zobj->type != REDIS_ZSET) {
            addReply(c,shared.wrongtypeerr);
            goto cleanup;
        }
    }

    // 處理所有元素
    for (j = 0; j < elements; j++) {
        score = scores[j];

        // 有序集合爲 ziplist 編碼
        if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
           ...
        // 有序集合爲 SKIPLIST 編碼
        } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
            zset *zs = zobj->ptr;
            zskiplistNode *znode;
            dictEntry *de;

            // 編碼對象
            ele = c->argv[3+j*2] = tryObjectEncoding(c->argv[3+j*2]);

            // 查看成員是否存在
            de = dictFind(zs->dict,ele);
            if (de != NULL) {

                // 成員存在

                // 取出成員
                curobj = dictGetKey(de);
                // 取出分值
                curscore = *(double*)dictGetVal(de);

                // ZINCRYBY 時執行
                ...
                
                /* Remove and re-insert when score changed. We can safely
                 * delete the key object from the skiplist, since the
                 * dictionary still has a reference to it. */
                // 執行 ZINCRYBY 命令時,
                // 或者用戶通過 ZADD 修改成員的分值時執行
                if (score != curscore) {
                    // 刪除原有元素
                    redisAssertWithInfo(c,curobj,zslDelete(zs->zsl,curscore,curobj));

                    // 重新插入元素
                    znode = zslInsert(zs->zsl,score,curobj);
                    incrRefCount(curobj); /* Re-inserted in skiplist. */

                    // 更新字典的分值指針
                    dictGetVal(de) = &znode->score; /* Update score ptr. */

                    server.dirty++;
                    updated++;
                }
            } else {

                // 元素不存在,直接添加到跳躍表
                znode = zslInsert(zs->zsl,score,ele);
                incrRefCount(ele); /* Inserted in skiplist. */

                // 將元素關聯到字典
                redisAssertWithInfo(c,NULL,dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
                incrRefCount(ele); /* Added to dictionary. */

                server.dirty++;
                added++;
            }
        } else {
            redisPanic("Unknown sorted set encoding");
        }
    }

    if (incr) /* ZINCRBY */
        addReplyDouble(c,score);
    else /* ZADD */
        addReplyLongLong(c,added);

cleanup:
   ...
}

/*
 * 創建一個 SKIPLIST 編碼的有序集合
 */
robj *createZsetObject(void) {

    zset *zs = zmalloc(sizeof(*zs));

    robj *o;

    zs->dict = dictCreate(&zsetDictType,NULL);
    zs->zsl = zslCreate();

    o = createObject(REDIS_ZSET,zs);

    o->encoding = REDIS_ENCODING_SKIPLIST;

    return o;
}

/*
 * 創建並返回一個新的跳躍表
 *
 * T = O(1)
 */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    // 分配空間
    zsl = zmalloc(sizeof(*zsl));

    // 設置高度和起始層數
    zsl->level = 1;
    zsl->length = 0;

    // 初始化表頭節點
    // T = O(1)
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;

    // 設置表尾
    zsl->tail = NULL;

    return zsl;
}

zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    
    // 分配空間
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));

    // 設置屬性
    zn->score = score;
    zn->obj = obj;

    return zn;
}

ZRANGE 取top N到 top M之間的成員

爲什麼redis選擇跳躍表而不是平衡二叉樹來實現有序集合

平衡二叉樹中有代表性的如 AVL樹,紅黑樹。
跳躍表相比於紅黑樹這種數據結構,衆所周知的優點就是,crud性能可以媲美紅黑樹,
數據結構簡單,實現難度大大小於紅黑樹。
那麼除了跳躍表簡單好實現以外,還有什麼別的特殊的原因讓redis選擇了跳躍表而不是紅黑樹?

對於這個問題的解答,如果redis大佬已經回答過的話,那直接看他怎麼回答是最合適的了😂
在這裏插入圖片描述
原文鏈接
redis的有序集合上經常有區間遍歷操作。通過跳躍表加速查找到區間頭,然後在鏈表上進行區間遍歷,性能和平衡二叉樹差不多。

疑問? 平衡二叉樹似乎也能按中序遍歷的順序,將各個節點連接起來,形成一條有序的鏈表,似乎也可行。
但是這種爲了遍歷而串成的鏈表,似乎沒有跳躍表的鏈表來的自然 🤔

ZRANGE 與跳躍表相關的源碼邏輯

源碼邏輯大概如下

  1. 一段操作,檢查參數,修正參數,算出start, end
  2. 通過跳躍表加速查找到 start節點的位置
  3. 從start 節點開始,遍歷鏈表

源碼如下👇

void zrangeCommand(redisClient *c) {
    zrangeGenericCommand(c,0);
}


void zrangeGenericCommand(redisClient *c, int reverse) {
    robj *key = c->argv[1];
    robj *zobj;
    int withscores = 0;
    long start;
    long end;
    int llen;
    int rangelen;

    // 取出 start 和 end 參數
    if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != REDIS_OK) ||
        (getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != REDIS_OK)) return;

    // 確定是否顯示分值
    if (c->argc == 5 && !strcasecmp(c->argv[4]->ptr,"withscores")) {
        withscores = 1;
    } else if (c->argc >= 5) {
        addReply(c,shared.syntaxerr);
        return;
    }

    // 取出有序集合對象
    if ((zobj = lookupKeyReadOrReply(c,key,shared.emptymultibulk)) == NULL
         || checkType(c,zobj,REDIS_ZSET)) return;

    /* Sanitize indexes. */
    // 將負數索引轉換爲正數索引
    llen = zsetLength(zobj);
    if (start < 0) start = llen+start;
    if (end < 0) end = llen+end;
    if (start < 0) start = 0;

    /* Invariant: start >= 0, so this test will be true when end < 0.
     * The range is empty when start > end or start >= length. */
    // 過濾/調整索引
    if (start > end || start >= llen) {
        addReply(c,shared.emptymultibulk);
        return;
    }
    if (end >= llen) end = llen-1;
    rangelen = (end-start)+1;

    /* Return the result in form of a multi-bulk reply */
    addReplyMultiBulkLen(c, withscores ? (rangelen*2) : rangelen);

    if (zobj->encoding == REDIS_ENCODING_ZIPLIST) {
       ...

    } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        zskiplist *zsl = zs->zsl;
        zskiplistNode *ln;
        robj *ele;

        /* Check if starting point is trivial, before doing log(N) lookup. */
        // 迭代的方向
        if (reverse) {
            ln = zsl->tail;
            if (start > 0)
                ln = zslGetElementByRank(zsl,llen-start);
        } else {
            ln = zsl->header->level[0].forward;
            if (start > 0)
                ln = zslGetElementByRank(zsl,start+1);
        }

        // 取出元素
        while(rangelen--) {
            redisAssertWithInfo(c,zobj,ln != NULL);
            ele = ln->obj;
            addReplyBulk(c,ele);
            if (withscores)
                addReplyDouble(c,ln->score);
            ln = reverse ? ln->backward : ln->level[0].forward;
        }
    } else {
        redisPanic("Unknown sorted set encoding");
    }
}

小結

  1. 有序集合中,成員是唯一的,但分數可以重複
  2. 有序集合中按分數從小到大,當分數相同按成員字典順序從小到大排列
  3. 有序集合中有一個雙向鏈表,所以可以雙向遍歷
  4. 有序集合適合用來做排行榜之類的功能,當然如果單個有序集合成員數過多,佔用的內存也會很大。
  5. 有序集合在3.0版本中,最高32層

往期博客回顧

  1. redis服務器的部分啓動過程
  2. GET命令背後的源碼邏輯
  3. redis的基礎數據結構之 sds
  4. redis的基礎數據結構之 list
  5. redis的基礎數據結構 之 ziplist
  6. redis 基礎數據結構之 hash表
  7. redis不穩定字典的遍歷
  8. redis集合的實現與 求交/並/差集
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章