《redis设计与实现》-8 有序集合zset

一 序

  书上本节照例比较简单,介绍两种编码及转换方式。所以还是分为编码跟命令实现两部分。

二  zset 

2.1 编码

有序集合对象的底层实现类型如下表:

编码—encoding对象—ptr
OBJ_ENCODING_SKIPLIST跳跃表和字典实现的有序集合对象
OBJ_ENCODING_ZIPLIST压缩列表实现的有序集合对象

关于底层的数据结构,参见:《redis设计与实现》-第7章压缩列表ziplist ,《redis设计与实现》-第5章跳跃表 ,《redis设计与实现》-4 字典 。

编码为 OBJ_ENCODING_SKIPLIST 的底层的数据结构是 跳跃表和字典。因此就抽象了一个zset结构用来保存这两种结构。源码在server.h

typedef struct zset {
    dict *dict;  //字典
    zskiplist *zsl; //跳跃表
} zset; //有序集合跳跃表类型
  • 跳跃表和平衡树的元素都是有序排列,而哈希表不是有序的。因此在哈希表上的查找只能是单个key的查找,不适合做范围查找。
  • 跳跃表和平衡树做范围查找时,跳跃表算法简单,实现方便,而平衡树逻辑复杂。
  • 查找单个key,跳跃表和平衡树的平均时间复杂度都为O(logN),而哈希表的时间复杂度为O(N)。
  • 下面贴一段书上的原话。

    为什么有序集合需要同时使用跳跃表和字典来实现?

    在理论上来说, 有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现, 但无论单独使用字典还是跳跃表, 在性能上对比起同时使用字典和跳跃表都会有所降低。

    举个例子, 如果我们只使用字典来实现有序集合, 那么虽然以 O(1) 复杂度查找成员的分值这一特性会被保留, 但是, 因为字典以无序的方式来保存集合元素, 所以每次在执行范围型操作 —— 比如 ZRANK 、 ZRANGE 等命令时, 程序都需要对字典保存的所有元素进行排序, 完成这种排序需要至少 O(N \log N) 时间复杂度, 以及额外的 O(N) 内存空间 (因为要创建一个数组来保存排序后的元素)。

    另一方面, 如果我们只使用跳跃表来实现有序集合, 那么跳跃表执行范围型操作的所有优点都会被保留, 但因为没有了字典, 所以根据成员查找分值这一操作的复杂度将从 O(1) 上升为 O(\log N) 。

    因为以上原因, 为了让有序集合的查找和范围型操作都尽可能快地执行, Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。

redis中实现有序集合的办法是:跳跃表+字典(哈希表)

2.2 结构

书上也有例子,但是我觉得网上的这个图更清晰易懂,就不用书上例子了。例子来自men_ven. 感谢原作者,真实用心画图整理了。我是喜欢偷懒,就是截个书上的图。

我们创建一个视频播放量的有序集合,有体育sport、卡通carton、电视剧telepaly这三个成员

127.0.0.1:6379> ZADD video:view 1234 sport 888 carton 2345 teleplay 
(integer) 3
127.0.0.1:6379> ZRANGE video:view 0 -1 WITHSCORES
1) "carton"
2) "88888"
3) "sport"
4) "123456"
5) "teleplay"
6) "234567"
127.0.0.1:6379> OBJECT ENCODING video:view
"ziplist"

我们可以查看到编码类型是压缩列表,这个 video:view 对象的空间结构如下图所示:

è¿éåå¾çæè¿°

当有序集合类型达到配置文件中的阈值条件时,就会发生编码类型的转换,转换为OBJ_ENCODING_SKIPLIST类型,而且转换是双向的,可以相互转换,默认的条件如下

#define OBJ_ZSET_MAX_ZIPLIST_ENTRIES 128 // ziplist中最多存放的节点数

#define OBJ_ZSET_MAX_ZIPLIST_VALUE 64 // ziplist中最大存放的数据长度

还是上面的例子使用OBJ_ENCODING_SKIPLIST编码方式展示可能的空间结构:

注意红色:BJ_ENCODING_SKIPLIST 编码方式的有序集合,他的成员在两种数据结构中是共享的,只是简单的增加了引用计数,实际上内存只有一份成员数据。所以不会造成任何数据重复,浪费内存。

有序集合基于ziplist的接口在server.h

zskiplist *zslCreate(void);//创建一个新的跳跃表。
void zslFree(zskiplist *zsl);//释放给定跳跃表,以及表中包含的所有节点
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj);//skiplist 插入节点
unsigned char *zzlInsert(unsigned char *zl, robj *ele, double score);//将ele元素和分值score插入在ziplist中,从小到大排序
int zslDelete(zskiplist *zsl, double score, robj *obj);//在一个zskiplist中删除一个节点
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range);//找到一个zskiplist中第一个出现在range中的节点
zskiplistNode *zslLastInRange(zskiplist *zsl, zrangespec *range);//找到一个zskiplist中最后一个出现在range中的节点
double zzlGetScore(unsigned char *sptr);// 从sptr指向的entry中取出有序集合的分值
void zzlNext(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);// 将当前的元素指针eptr和当前元素分值的指针sptr都指向下一个元素和元素的分值
void zzlPrev(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);// 将当前的元素指针eptr和当前元素分值的指针sptr都指向上一个元素和元素的分值
unsigned int zsetLength(robj *zobj);// 返回有序集合的元素个数
void zsetConvert(robj *zobj, int encoding);// 将有序集合对象的编码转换为encoding制定的编码类型
void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen);// 按需转换编码成OBJ_ENCODING_ZIPLIST
int zsetScore(robj *zobj, robj *member, double *score);// 将有序集合的member成员的分值保存到score中
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o);//计算排位

2.3有序集合的迭代器

有序集合在t_zset.c中定义了迭代器,迭代器适用于有序集合和集合

// 集合类型的迭代器
typedef struct {
    robj *subject;                  //所属的对象
    int type; /* Set, sorted set */ //对象的类型,可以是集合或有序集合
    int encoding;                   //编码
    double weight;                  //权重

    union {
        /* Set iterators. */        //集合迭代器
        union _iterset {
            struct {                //intset迭代器
                intset *is;         //所属的intset
                int ii;             //intset节点下标
            } is;
            struct {                //字典迭代器
                dict *dict;         //所属的字典
                dictIterator *di;   //字典的迭代器
                dictEntry *de;      //当前指向的字典节点
            } ht;
        } set;

        /* Sorted set iterators. */         //有序集合迭代器
        union _iterzset {                   //ziplist迭代器
            struct {
                unsigned char *zl;          //所属的ziplist
                unsigned char *eptr, *sptr; //当前指向的元素和分值
            } zl;
            struct {                        //zset迭代器
                zset *zs;                   //所属的zset
                zskiplistNode *node;        //当前指向的跳跃节点
            } sl;
        } zset;
    } iter;
} zsetopsrc;
// 集合迭代器和有序集合迭代器是一个共用体union,因此,不会浪费空间。

在实现迭代的过程中,由于迭代器指向的节点可能数据类型各不相同,有对象、有字符串、有整数,因此Redis又定义了一个保存这些类型的统一结构:源码在t_zset.c

/* Store value retrieved from the iterator. */
// 从集合类型迭代器中恢复存储的值
typedef struct {
    int flags;
    unsigned char _buf[32]; /* Private buffer. */
    // 保存元素的各种类型
    robj *ele;              //对象
    unsigned char *estr;    //ziplist
    unsigned int elen;
    long long ell;
    // 保存分值
    double score;
} zsetopval;

定义了zsetopval结构,还可以实现内部各种类型的数据进行转换,例如:根据estr的值,转换成ele对象类型。

当然,还为迭代器定义了一些接口,使用起来方便,函数名字都是以zui开头,实现是t_zset.c

 

2.4. 有序集合的范围限定方式

Redis有序集合有三种范围限定

  • 根据字典区间
  • 根据排位区间
  • 根据分值区间

以上三种主要依赖两种排序方式

  • 分值序
  • 字典序

以上两种排序所表示的范围,依赖于两个结构,一个规定分值序的范围和边界,一个规定字典序的范围和边界。源码在server.h

/* Struct to hold a inclusive/exclusive range spec by score comparison. */
//分值排序的范围和边界
typedef struct {
    double min, max;//最小值,最大值
    //minex表示最小值是否包含在这个范围,1表示不包含,0表示包含
    //maxex表示最小值是否包含在这个范围,1表示不包含,0表示包含
    int minex, maxex; /* are min or max exclusive? */
} zrangespec;

/* Struct to hold an inclusive/exclusive range spec by lexicographic comparison. */
//  字典排序的范围和边界
typedef struct {
    robj *min, *max;  /* May be set to shared.(minstring|maxstring)  最小值、最大值*/
    //minex表示最小值是否包含在这个范围,1表示不包含,0表示包含
    //maxex表示最小值是否包含在这个范围,1表示不包含,0表示包含
    int minex, maxex; /* are min or max exclusive? */
} zlexrangespec;

三 命令实现

看了下代码,有序集合代码大概是集合的3倍,加注释3000行左右。所以重点关注两类命令。

3.1 求交集zinterstore, 求并集zunionstore

void zunionstoreCommand(client *c) {
    zunionInterGenericCommand(c,c->argv[1], SET_OP_UNION);
}

void zinterstoreCommand(client *c) {
    zunionInterGenericCommand(c,c->argv[1], SET_OP_INTER);
}
void zunionInterGenericCommand(client *c, robj *dstkey, int op) {
    int i, j;
    long setnum;
    int aggregate = REDIS_AGGR_SUM;  //默认聚合方式是求和
    zsetopsrc *src;
    zsetopval zval;
    robj *tmp;
    unsigned int maxelelen = 0;
    robj *dstobj;
    zset *dstzset;
    zskiplistNode *znode;
    int touched = 0;

    /* expect setnum input keys to be given */
    //取出要遍历的集合的个数
    if ((getLongFromObjectOrReply(c, c->argv[2], &setnum, NULL) != C_OK))
        return;//不存在,返回

    if (setnum < 1) {
        addReplyError(c,
            "at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE");
        return;
    }

    /* test if the expected number of keys would overflow */
    if (setnum > c->argc-3) {//参数不匹配,提示语法错误
        addReply(c,shared.syntaxerr);
        return;
    }

    /* read keys to be used for input */
    //为每一个key创建一个迭代器
    src = zcalloc(sizeof(zsetopsrc) * setnum);
    for (i = 0, j = 3; i < setnum; i++, j++) {// why j =3?
        robj *obj = lookupKeyWrite(c->db,c->argv[j]);//以写的方式读出有序集合
        if (obj != NULL) {
            if (obj->type != OBJ_ZSET && obj->type != OBJ_SET) {//类型判断是否集合
                zfree(src); //释放
                addReply(c,shared.wrongtypeerr);//返回client 类型错误
                return;
            }
             //赋值每个集合对象的迭代器
            src[i].subject = obj;
            src[i].type = obj->type;
            src[i].encoding = obj->encoding;
        } else {
            src[i].subject = NULL;
        }

        /* Default all weights to 1. */
        //默认权重为1.0
        src[i].weight = 1.0;
    }

    /* parse optional extra arguments */
    //解析扩展参数
    if (j < c->argc) {
        int remaining = c->argc - j;

        while (remaining) {
        	  //解析权重参数
            if (remaining >= (setnum + 1) && !strcasecmp(c->argv[j]->ptr,"weights")) {
                j++; remaining--;
                //遍历设置的权重参数,设置到集合迭代器中
                for (i = 0; i < setnum; i++, j++, remaining--) {
                    if (getDoubleFromObjectOrReply(c,c->argv[j],&src[i].weight,
                            "weight value is not a float") != C_OK)
                    {
                        zfree(src);
                        return;
                    }
                }
            } else if (remaining >= 2 && !strcasecmp(c->argv[j]->ptr,"aggregate")) {//解析聚合方式aggregate参数
                j++; remaining--;
                if (!strcasecmp(c->argv[j]->ptr,"sum")) {//求和
                    aggregate = REDIS_AGGR_SUM;
                } else if (!strcasecmp(c->argv[j]->ptr,"min")) {//最小
                    aggregate = REDIS_AGGR_MIN;
                } else if (!strcasecmp(c->argv[j]->ptr,"max")) {//最大
                    aggregate = REDIS_AGGR_MAX;
                } else {//语法错误
                    zfree(src);
                    addReply(c,shared.syntaxerr);
                    return;
                }
                j++; remaining--;
            } else {//不是以上两种参数,发送语法错误信息
                zfree(src);
                addReply(c,shared.syntaxerr);
                return;
            }
        }
    }

    /* sort sets from the smallest to largest, this will improve our
     * algorithm's performance */
     // 对所有集合进行排序(按元素的从小到大),以提高算法性能
    qsort(src,setnum,sizeof(zsetopsrc),zuiCompareByCardinality);
    
    //创建结果集对象
    dstobj = createZsetObject();
    dstzset = dstobj->ptr;
    memset(&zval, 0, sizeof(zval)); //初始化保存有序集合元素和分值的结构

    if (op == SET_OP_INTER) { // ZINTERSTORE命令
        /* Skip everything if the smallest input is empty. */
        if (zuiLength(&src[0]) > 0) { // 只处理非空集合
            /* Precondition: as src[0] is non-empty and the inputs are ordered
             * by size, all src[i > 0] are non-empty too. */
              // 初始化元素最少的有序集合
            zuiInitIterator(&src[0]);
             // 遍历元素最少的有序集合src[0]的每一个元素,将元素的分值和数值保存到zval中
            while (zuiNext(&src[0],&zval)) {
                double score, value;
	              // 计算加权分值
                score = src[0].weight * zval.score;
                if (isnan(score)) score = 0;//非数字则分数为0

              // 遍历其后的每一个有序集合,分别跟元素最少的有序集合做运算
                for (j = 1; j < setnum; j++) {
                    /* It is not safe to access the zset we are
                     * iterating, so explicitly check for equal object. */
                      // 如果输入的key一样(这里有个条件:在某个 key 输入了两次,并且这个 key 是所有输入集合中基数最小的集合时会出)
                      //满足这个条件的情况下,那么我们可以直接计算聚合值,不必进行 zuiFind 去确保元素是否出现
                    if (src[j].subject == src[0].subject) {
                    	   // 计算新的加权分值
                        value = zval.score*src[j].weight;
                         // 根据集合方式sum、min、max保存结果值到score
                        zunionInterAggregate(&score,value,aggregate);
                          //key不同的情况下, 如果能在其他集合找到当前迭代到的元素的话
                    } else if (zuiFind(&src[j],&zval,&value)) {
                        value *= src[j].weight;// 计算加权后的分值
                        zunionInterAggregate(&score,value,aggregate); // 根据集合方式sum、min、max保存结果值到score
                    } else {// 如果当前元素没出现在某个集合,那么跳出 for 循环,处理下个元素
                        break;
                    }
                }

                /* Only continue when present in every input. */
                //走到这里,没有跳出for循环,那么该元素一定是每个key集合中都存在,才将此元素添加进结果集合中
                if (j == setnum) {
                	  // 取出值对象
                    tmp = zuiObjectFromValue(&zval);
                    znode = zslInsert(dstzset->zsl,score,tmp); //插入到有序集合中
                    incrRefCount(tmp); /* added to skiplist 加入到跳表*/
                    dictAdd(dstzset->dict,tmp,&znode->score);//加入到字典中
                    incrRefCount(tmp); /* added to dictionary */

                    if (sdsEncodedObject(tmp)) {//如果是字符串编码的对象
                        if (sdslen(tmp->ptr) > maxelelen)//更新最大长度
                            maxelelen = sdslen(tmp->ptr);
                    }
                }
            }
            zuiClearIterator(&src[0]);//释放集合类型的迭代器
        }
    } else if (op == SET_OP_UNION) {  // ZUNIONSTORE命令
        dict *accumulator = dictCreate(&setDictType,NULL);//创建一个临时字典作为结果集字典
        dictIterator *di;
        dictEntry *de;
        double score;
        
        if (setnum) {
            /* Our union is at least as large as the largest set.
             * Resize the dictionary ASAP to avoid useless rehashing. */
              // 为了尽可能的减少rehash操作,计算出最大有序集合元素个数,并集至少和最大的集合一样大(上面已经排序过, 最后一个是最大的集合),因此按需扩展结果集字典
            dictExpand(accumulator,zuiLength(&src[setnum-1]));
        }

        /* Step 1: Create a dictionary of elements -> aggregated-scores
         * by iterating one sorted set after the other. */
           // 1.遍历所有的集合
        for (i = 0; i < setnum; i++) {
            if (zuiLength(&src[i]) == 0) continue; // 跳过空集合

            zuiInitIterator(&src[i]);//初始化集合迭代器
            while (zuiNext(&src[i],&zval)) {// 遍历当前集合的所有元素,迭代器当前指向的元素保存在zval中
                /* Initialize value */
                score = src[i].weight * zval.score; //初始化分值
                if (isnan(score)) score = 0;//非数字(可能存在溢出的可能)为0

                /* Search for this element in the accumulating dictionary. */
                /* 查找元素是否已经在结果集(accumulator)字典中 */
                de = dictFind(accumulator,zuiObjectFromValue(&zval));
                /* If we don't have it, we need to create a new entry. */
                // 如果在结果集中找不到,则加入到结果集中
                if (de == NULL) {
                    tmp = zuiObjectFromValue(&zval);//创建元素对象
                    /* Remember the longest single element encountered,
                     * to understand if it's possible to convert to ziplist
                     * at the end. */
                      /* 记录元素最长的值, 后面用于判断是否需要对集合进行转换*/
                    if (sdsEncodedObject(tmp)) { //字符串类型的话要更新maxelelen
                        if (sdslen(tmp->ptr) > maxelelen)
                            maxelelen = sdslen(tmp->ptr);
                    }
                    /* Add the element with its initial score. */
                    /* 直接添加到结果集字典中 */
                    de = dictAddRaw(accumulator,tmp);
                    incrRefCount(tmp);
                    dictSetDoubleVal(de,score);//设置元素的分值
                } else {// 如果在结果集中可以找到
                    /* Update the score with the score of the new instance
                     * of the element found in the current sorted set.
                     *
                     * Here we access directly the dictEntry double
                     * value inside the union as it is a big speedup
                     * compared to using the getDouble/setDouble API. */
                      //更新加权分值
                    zunionInterAggregate(&de->v.d,score,aggregate);
                }
            }
            zuiClearIterator(&src[i]);//清理当前集合的迭代器
        }

        /* Step 2: convert the dictionary into the final sorted set.
         */  把字典变成有序集合
         // 2. 创建字典的迭代器,遍历结果集字典
        di = dictGetIterator(accumulator);

        /* We now are aware of the final size of the resulting sorted set,
         * let's resize the dictionary embedded inside the sorted set to the
         * right size, in order to save rehashing time. */
         // 按需扩展结果集合中的字典成员
        dictExpand(dstzset->dict,dictSize(accumulator));

        // 遍历结果集字典,
        while((de = dictNext(di)) != NULL) {
        	 // 获得元素对象和分值
            robj *ele = dictGetKey(de);
            score = dictGetDoubleVal(de);
              // 插入到结果集合的跳跃表中
            znode = zslInsert(dstzset->zsl,score,ele);
            incrRefCount(ele); /* added to skiplist */
             // 添加到结果集合的字典中,注意,这里才是真正的结果集字典,上面accumulator是临时的,不要被注释误会
            dictAdd(dstzset->dict,ele,&znode->score);
            incrRefCount(ele); /* added to dictionary */
        }
        dictReleaseIterator(di); //释放字典的迭代器

        /* We can free the accumulator dictionary now. */
        dictRelease(accumulator);//释放临时保存结果的字典
    } else {
        serverPanic("Unknown operator");
    }

    if (dbDelete(c->db,dstkey))// 删除已经存在的dstkey,并做标记
        touched = 1;
    if (dstzset->zsl->length) {// 如果结果集合的长度不为 0 
        zsetConvertToZiplistIfNeeded(dstobj,maxelelen); //是否需要进行编码转换
        dbAdd(c->db,dstkey,dstobj);//将数据库中的dstkey和dstobj关联成对
        addReplyLongLong(c,zsetLength(dstobj)); // 回复结果集合的长度给client
        signalModifiedKey(c->db,dstkey);//发送键被修改的信号
        notifyKeyspaceEvent(NOTIFY_ZSET,
            (op == SET_OP_UNION) ? "zunionstore" : "zinterstore",
            dstkey,c->db->id);//发送对应的事件通知 zunionstore or zinterstore
        server.dirty++;//更新脏键
    } else {//结果集为空
        decrRefCount(dstobj);//释放对象
        addReply(c,shared.czero);//发送0给client
        if (touched) {
            signalModifiedKey(c->db,dstkey);
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",dstkey,c->db->id);
            server.dirty++;
        }
    }
    zfree(src);//释放集合迭代器数组空间
}

有序集合计算交集的一般算法:先将所有集合按元素个数从小到大排序,然后以元素个数最少的集合,也就是第一个集合为基数,遍历该集合的所有元素,如果元素在之后的所有集合都存在,则加入结果集中。

有序集合计算并集的一般算法:先将所有集合按元素个数从小到大排序,遍历所有的集合的所有元素,将元素添加到结果集中,如果元素已不存在,则添加,否则操作下一个元素。

3.2 zrange:

   获取一个位置范围内的元素, 命令格式: ZRANGE key start stop [WITHSCORES], start, stop元素代表位置下标, 从0开始.
下标参数 start 和 stop 都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。

源码在t_zset.c 

//获取一个位置范围内的元素, 命令格式: ZRANGE key start stop [WITHSCORES]
void zrangeCommand(client *c) {
    zrangeGenericCommand(c,0);
}

void zrevrangeCommand(client *c) {
    zrangeGenericCommand(c,1);
}

底层的实现是zrangeGenericCommand.

void zrangeGenericCommand(client *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) != C_OK) ||
        (getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != C_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,OBJ_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;
    }
    //超出范围:end 设置为集合尾元素
    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 == OBJ_ENCODING_ZIPLIST) {//ziplist
        unsigned char *zl = zobj->ptr;
        unsigned char *eptr, *sptr;
        unsigned char *vstr;
        unsigned int vlen;
        long long vlong;

         /* ziplist首先找到start位置的元素, 然后依次遍历rangelen个元素, 返回给客户端*/
        if (reverse)
            eptr = ziplistIndex(zl,-2-(2*start));
        else
            eptr = ziplistIndex(zl,2*start);

        serverAssertWithInfo(c,zobj,eptr != NULL);
        sptr = ziplistNext(zl,eptr);

        while (rangelen--) {//取出元素
            serverAssertWithInfo(c,zobj,eptr != NULL && sptr != NULL);
            serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
            if (vstr == NULL)
                addReplyBulkLongLong(c,vlong);
            else
                addReplyBulkCBuffer(c,vstr,vlen);

            if (withscores)
                addReplyDouble(c,zzlGetScore(sptr));

            if (reverse)
                zzlPrev(zl,&eptr,&sptr);
            else
                zzlNext(zl,&eptr,&sptr);
        }

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

        /* Check if starting point is trivial, before doing log(N) lookup. */
         /* ziplist首先找到start位置的元素, 然后依次遍历rangelen个元素, 返回给客户端*/
        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--) {//取出元素
            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;
        }
    } else {
        serverPanic("Unknown sorted set encoding");
    }
}

跟跳表有关的是:

/* Finds an element by its rank. The rank argument needs to be 1-based. */
//根据排位在跳跃表中查找元素。排位的起始值为 1 。
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)
        {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
         // 如果越过的节点数量已经等于 rank,就找到了
        if (traversed == rank) {
            return x;
        }
    }
    return NULL;
}

看起来代码很长,除了开始的校验之外,是区分了ziplist 跟跳表的。

3.3 ZRANK ZREVRANK命令底层实现

ZRANK key member

返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。排名以 0 为底,也就是说, score 值最小的成员排名为 0 。


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

void zrevrankCommand(client *c) {
    zrankGenericCommand(c, 1);
}
void zrankGenericCommand(client *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,OBJ_ZSET)) return;
    llen = zsetLength(zobj); // 元素数量

    serverAssertWithInfo(c,ele,sdsEncodedObject(ele));//参数类型校验

    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {//ziplist
        unsigned char *zl = zobj->ptr;
        unsigned char *eptr, *sptr;

        eptr = ziplistIndex(zl,0);//元素节点地址
        serverAssertWithInfo(c,zobj,eptr != NULL);
        sptr = ziplistNext(zl,eptr);//分值节点地址
        serverAssertWithInfo(c,zobj,sptr != NULL);

        rank = 1;//排名位置
        while(eptr != NULL) {
        	    // 比较当前元素和member参数的大小
            if (ziplistCompare(eptr,ele->ptr,sdslen(ele->ptr)))
                break;
            rank++;//排名+1
            zzlNext(zl,&eptr,&sptr); //指向下个元素和分值
        }
	        // 发送排位值
        if (eptr != NULL) {// ZRANK 还是 ZREVRANK ?
            if (reverse)
                addReplyLongLong(c,llen-rank);
            else
                addReplyLongLong(c,rank-1);
        } else {
            addReply(c,shared.nullbulk);
        }
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        zset *zs = zobj->ptr;
        zskiplist *zsl = zs->zsl;
        dictEntry *de;
        double score;

        ele = c->argv[2];//member参数
        de = dictFind(zs->dict,ele); // 从字典中取出保存member的节点地址
        if (de != NULL) {
            score = *(double*)dictGetVal(de);//获取分值
            rank = zslGetRank(zsl,score,ele);//计算排名
            serverAssertWithInfo(c,ele,rank); /* Existing elements always have a rank. */
              // 发送排位
            if (reverse) // ZRANK 还是 ZREVRANK ?
                addReplyLongLong(c,llen-rank);
            else
                addReplyLongLong(c,rank-1);
        } else {
            addReply(c,shared.nullbulk);
        }
    } else {
        serverPanic("Unknown sorted set encoding");
    }
}

其中跳跃表查找排位 方法如下:

/* Find the rank for an element by both score and key.
 * Returns 0 when the element cannot be found, rank otherwise.
 * Note that the rank is 1-based due to the span of zsl->header to the
 * first element.查找包含给定分值和成员对象的节点在跳跃表中的排位。 
 */
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 */
        // 必须确保不仅分值相等,而且成员对象也要相等
        if (x->obj && equalStringObjects(x->obj,o)) {
            return rank;
        }
    }
    return 0;
}

 

 

参考:

https://blog.csdn.net/men_wen/article/details/71043205

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值