• redis-zset数据结构探索


    redis用的人比较多,其中zset大家都熟悉,主要用于排名场景。
    zset数据结构,分成两部分,一部分是用于排序,一部分用于缓存键值。
    先看看结构:

    typedef struct zset {
        dict *dict;         //缓存
        zskiplist *zsl;     //排序结构
    } zset;

    上面,跳跃表用于排序结构,可以按照名次,积分查找对应键, 时间复杂度: log(n)。
    按照名次,积分范围查找一系列键时, 先查询满足条件的第一个键,然后当前键查找后续键, 时间复杂度: log(n) + o(m), n=总键数, m=查询结果键数。

    跳跃表结构:

    typedef struct zskiplist {
        struct zskiplistNode *header, *tail;        //结点:用于顺序查询,常用方式; 结点:用于倒序简单查询。
        unsigned long length;                       //结点数
        int level;                                  //跳跃层级
    } zskiplist;

    结点结构:

    typedef struct zskiplistNode {
        robj *obj;                                  //
        double score;                               //积分
        struct zskiplistNode *backward;             //前一个结点, 和level[0]可看作双链表
        struct zskiplistLevel {                     //跳跃层关系, 每层都是单链表
            struct zskiplistNode *forward;          //此层下一个结点
            unsigned int span;                      //此层下一个结点和当前结点距离(两者隔了多少结点)
        } level[];                                  //最多32层
    } zskiplistNode; 


    查询:
    根据名次范围查询

    void zrangeGenericCommand(client *c, int reverse) {
            ......
    
            zset *zs = zobj->ptr;           //zset结构变量
            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);       //如果start>0, 则取对应结点
            } else {
                ln = zsl->header->level[0].forward; //默认取第一个结点
                if (start > 0)
                    ln = zslGetElementByRank(zsl,start+1);
            }
    
            while(rangelen--) {             //取rangelen个结点
                serverAssertWithInfo(c,zobj,ln != NULL);
                ele = ln->obj;
                addReplyBulk(c,ele);        //响应键名
                if (withscores)
                    addReplyDouble(c,ln->score);    //响应键值
                ln = reverse ? ln->backward : ln->level[0].forward; //设置下一个结点
            }
    
            ......
    }
    
    /* Finds an element by its rank. The rank argument needs to be 1-based. */
    zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
        zskiplistNode *x;
        unsigned long traversed = 0;                //当前名次
        int i;
    
        x = zsl->header;                            //头结点, 从头结点的下一个结点遍历
        for (i = zsl->level-1; i >= 0; i--) {       //从高层到低层链表遍历
            while (x->level[i].forward && (traversed + x->level[i].span) <= rank)   //如果有下一个结点,且下一个结点的名次<=rank
            {
                traversed += x->level[i].span;
                x = x->level[i].forward;
            }
            if (traversed == rank) {                //找到对应名次的结点
                return x;
            }
        }
        return NULL;
    }

     zslGetElementByRank()时间复杂度理想值 = log(n)

     
    如果有删除,添加操作,和查询类似,需要额外维护跳跃表关系。

    /* Delete all the elements with score between min and max from the skiplist.
     * Min and max are inclusive, so a score >= min || score <= max is deleted.
     * Note that this function takes the reference to the hash table view of the
     * sorted set, in order to remove the elements from the hash table too. 
     * 根据积分范围删除结点
    */ unsigned long zslDeleteRangeByScore(zskiplist *zsl, zrangespec *range, dict *dict) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; //update维护跳跃表层级关系,用于zslDeleteNode() unsigned long removed = 0; int i; x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { while (x->level[i].forward && (range->minex ? //不満足积分条件时,循环 x->level[i].forward->score <= range->min : x->level[i].forward->score < range->min)) x = x->level[i].forward; update[i] = x; //此层最接近于条件的结点 } /* Current node is the last with score < or <= min. */ x = x->level[0].forward; //第一个最可能满足条件的结点 /* Delete nodes while in range. */ while (x && (range->maxex ? x->score < range->max : x->score <= range->max)) //满足条件的结点 { zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl,x,update); //更新层级关系 dictDelete(dict,x->obj); //删除缓存 zslFreeNode(x); removed++; x = next; } return removed; } /* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */ void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i; for (i = 0; i < zsl->level; i++) { //更新层级关系 if (update[i]->level[i].forward == x) { update[i]->level[i].span += x->level[i].span - 1; update[i]->level[i].forward = x->level[i].forward; } else { update[i]->level[i].span -= 1; } } if (x->level[0].forward) { //维护当前结点的下一个结点 x->level[0].forward->backward = x->backward; } else { zsl->tail = x->backward; //维护尾结点 } while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL) zsl->level--; //维护层数 zsl->length--; //维护结点数 }

    根据键名查找积分, 有了zset->dict这个键值缓存,只需要时间复杂度0(1)

    int zsetScore(robj *zobj, robj *member, double *score) {
        if (!zobj || !member) return C_ERR;
    
        if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {       //ziplit实现
            if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR;
        } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
            zset *zs = zobj->ptr;
            dictEntry *de = dictFind(zs->dict, member);     //找到缓存entry
            if (de == NULL) return C_ERR;
            *score = *(double*)dictGetVal(de);              //获取对应积分  
        } else {
            serverPanic("Unknown sorted set encoding");
        }
        return C_OK;
    }

    如果zset对象用ziplist实现,则查询和删除操作时间复杂度 = o(n)

    ...

  • 相关阅读:
    Daily Scrum02 12.05
    Daily Scrum02 12.04
    用户调研报告
    Daily Scrum02 12.03
    Daily Scrum02 12.02
    Daily Scrum02 12.01
    Daily Scrum02 11.30
    软件工程项目组Z.XML会议记录 2013/11/27
    Daily Scrum02 11.29
    201509-3 模板生成系统
  • 原文地址:https://www.cnblogs.com/ginkgo-leaf/p/8809555.html
Copyright © 2020-2023  润新知