【Redis 六】Redis的有序集合类型(sortedset)

本文详细介绍了Redis的有序集合(sorted set)的高级特性,包括它的两种编码方式ziplist和skiplist的工作原理,以及如何通过score实现元素的有序排列。有序集合适用于创建排行榜、获取Top N数据等场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      在写redis系列的第一篇《【Redis 一】Jedis操作Redis有序集合类型(SortedSet)》中就已经单独的用着最高级的操作来开篇了,然后再用一篇来解读 何为高级。

   一、sortedset的高级

      第一点,是sortedset 如其名,既是拥有了set集合对象的特性,还是有序的,怎么实现的有序呢?它增加了一个分值(权重)score来设置单独的value,使得集合中的元素能够按score进行有序排列。

比set集合对象多了那么一点点就是高级了,社会资源会在头部,而头部只有20%,唯有每次优秀一点点,长期以往才可能在头部的位置。

  第二点,sortedset的两种编码分别是ziplist和skiplist。

//跳跃表和字典
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
//压缩列表     
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */         
#ziplist 和skiplist 的编码输出
压缩列表    REDIS_ENCODING_ZIPLIST  "ziplist"
跳跃表和字典  REDIS_ENCODING_SKIPLIST "skiplist"
/*有序集合对象和其编码类型*/
#使用压缩列表实现的有序集合对象。
REDIS_ZSET  REDIS_ENCODING_ZIPLIST  
使用跳跃表和字典实现的有序集合对象。
REDIS_ZSET  REDIS_ENCODING_SKIPLIST 

什么时候使用ziplist呢?满足以下两个条件?:

  1. 元素个数小于128个(刚好是四分之一的列表对象的限制[512])
  2. 其中每一个number的长度小于64字节

在redis配置文件redis.conf中可以通过设置zset-max-ziplist-entries和zset-max-ziplist-value来修改

使用ziplist编码的有序集合对象sortset通过ziplist是由两个紧挨在一起链表节点(listnode)表示的,我们从 添加一个有序元素 zadd key score member 命令上就可以发现 key,score,member是同时设置的,score和member是同时作为value被存在list里面,member放在第一个节点,score放在第二个节点。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。

   当超出设定的元素个数或者member的长度时则使用了skiplist编码的方式来实现。使用skiplist编码的有序集合底层是一个命名为zset的结构体,而一个zset结构同时包含一个字典和一个跳跃表。如下图所表示?:

#有序集合
typedef struct zset {
    // 字典,键为成员,值为分值,用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;

    // 跳跃表,按分值排序成员,用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;
} zset;

 

skiplist 跳跃表 是有序的数据结构,通过每个节点维持多个指向其他节点的指针来实现快速访问节点。skiplist 跳跃表 有两种结构 redis.h/zskiplistNode 和 redis.h/zskiplist。   

#zskiplistNode结构
typedef struct zskiplistNode {
    robj *obj;          //成员对象
    double score;       //分值
    struct zskiplistNode *backward;     //后退指针
    
    //层
    struct zskiplistLevel {
        struct zskiplistNode *forward;  //前进指针
        unsigned int span;              //跨度
    } level[];
} zskiplistNode;

  来一一看看上面的结构体各个属性?:

     每一个zskiplistNode都有的obj 成员对象和score,obj成员对象是一个指针,指向一个字符串对象,而字符串对象保存着一个SDS(简单动态字符串)值。score 分值是一个double 类型的浮点数,精度只有16位,跳跃表中的所有节点都按照分值从小到大排序。

每一个zskiplistNode的成员对象必须唯一,多个节点保存的分值可以是相同的。

分值相同的节点按照成员对象在字典序中的大小来排序,成员对象较小的节点会排在前面(靠近表头的方向).。

       backward后退指针作用是从表尾向表头访问节点,每个zskiplistNode只有一个backward后退指针。需要和一次可以跳过多个节点的前进指针做开区别。

      zskiplistLevel 层 是用来存储多个元素的数组,这个数组里面的每个元素都包含一个指向其他节点的指针,而这些指针就是访问的路径,通过这些指针来快速访问其他的节点。也就是说层的数量越多,其中包含的指针越多,访问其他节点的速度也就越快。

     每次创建一个新的跳跃表节点的时候,程序会根据 幂此定律(越大的数出现的概率越小)随机生成一个位于1~32之间的数作为层 zskiplistLevel 的大小。

     forward 前进指针 用于从表头向表尾方向访问节点,可以一次跳过多个节点,在每一个zskiplistLevel中都有一个指向表尾的指针。

       span 跨度 用于记录两个节点之间的距离,两个节点的距离越大,则跨度越大,指向null的节点的跨度为0.

#zskiplist 用来管理跳跃表节点
typedef struct zskiplist {
    //表头和表尾指针
    struct zskiplistNode *header, *tail;
    //节点的数量
    unsigned long length;   
    //层数最大的节点的层数
    int level;     
} zskiplist;

      表头节点并没有算到 节点数量里面,表头节点和其他节点的构造是一样的:有前进指针、后退指针、分值和成员对象,不过这些属性都不会用到,所以图中省略这些部分,只显示表头节点的各层。

      跳跃表支持平均O(log)、最坏O(N)复杂度的节点查找。 大部分情况下,跳跃表的效率可以和平衡树媲美,并且跳跃表的实现比平衡树更为简单。Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串,Redis 会使用跳跃表来作为有序集合的底层实现。Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构

 二、Zset的存储过程

       以zadd的操作作为例子如下(借鉴)?:

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;
    }

    // 取出所有输入的 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;
    }

    // 取出有序集合对象
    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) {
            unsigned char *eptr;

            // 查找成员
            ele = c->argv[3+j*2];
            if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {

                // 成员已存在

                // ZINCRYBY 命令时使用
                if (incr) {
                    score += curscore;
                    if (isnan(score)) {
                        addReplyError(c,nanerr);
                        goto cleanup;
                    }
                }

                // 执行 ZINCRYBY 命令时,
                // 或者用户通过 ZADD 修改成员的分值时执行
                if (score != curscore) {
                    // 删除已有元素
                    zobj->ptr = zzlDelete(zobj->ptr,eptr);
                    // 重新插入元素
                    zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                    // 计数器
                    server.dirty++;
                    updated++;
                }
            } else {
                // 元素不存在,直接添加
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);

                // 查看元素的数量,
                // 看是否需要将 ZIPLIST 编码转换为有序集合
                if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
                    zsetConvert(zobj,REDIS_ENCODING_SKIPLIST);

                // 查看新添加元素的长度
                // 看是否需要将 ZIPLIST 编码转换为有序集合
                if (sdslen(ele->ptr) > server.zset_max_ziplist_value)
                    zsetConvert(zobj,REDIS_ENCODING_SKIPLIST);

                server.dirty++;
                added++;
            }

        // 有序集合为 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 时执行
                if (incr) {
                    score += curscore;
                    if (isnan(score)) {
                        addReplyError(c,nanerr);

                        goto cleanup;
                    }
                }

                // 执行 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:
    zfree(scores);
    if (added || updated) {
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,
            incr ? "zincr" : "zadd", key, c->db->id);
    }
}

三、Redis的sortset 操作

  private void setSortSet(String key){
        //增加成员 zadd key score member
        Long zadd1 = getJedis().zadd(key, 215, "tom");
        getJedis().zadd(key,75,"alina");
        Long zadd2 = getJedis().zadd(key, 200, "kim");

        //计算成员个数   zcard key
        Long zcard = getJedis().zcard(key);

        //计算某个成员的分数,如果成员不存在,则返回nil zscore key member
        Double zscore = getJedis().zscore(key, "tom");

        //计算成员的排名,zrank是分数从低到高,zrevrank从高到低
        //zrank key member 和 zrevrank key member
        Long zrank = getJedis().zrank(key, "tom");
        Long zrevrank = getJedis().zrevrank(key, "tom");


        //删除成员zrem key member 将成员tom从有序集合key中删除
        Long zrem = getJedis().zrem(key, "tom");

        //增加成员的分数 zincrby key increment member
        //给kim 增加了20分,变成了220分 ,返回的为现在的分数
        Double zincrby = getJedis().zincrby(key, 20, "kim");

        //返回指定排名范围的成员 zrange key start end [withscores] //从低到高返回
        Set<String> zrange = getJedis().zrange(key, 0, 300);
        //从高到低返回
        Set<String> zrevrange = getJedis().zrevrange(key, 0, 300);
        //连同分数一起返回 zrange key 0 2 withscores
        Set<Tuple> zrangeWithScores = getJedis().zrangeWithScores(key, 0, 300);

        //返回指定分数范围的成员 zrangebyscore key min max [withscores] [limit offset count]
        Set<Tuple> tuples = getJedis().zrangeWithScores(key, 1, 220);

        //?上面为从低到高,此反之 zrevrangebyscore key max min [withscores] [limit offset count]
        Set<Tuple> tuples1 = getJedis().zrevrangeWithScores(key, 0, 220);

        //返回指定分数范围成员个数
        Long zcount = getJedis().zcount(key, 0, 100);

        //删除指定排名内的升序元素 zremrangebyrank key start end
        Long zremrangeByRank = getJedis().zremrangeByRank(key, 0, 10);

        //删除指定分数范围的成员 zremrangebyscore key min max
        Long zremrangeByScore = getJedis().zremrangeByScore(key, 0, 100);

        //求两个集合交集 zinterstore destination numkeys key [key ...] [weights weight [weight ...]]
        String otherSetKey = "otherSetKey";
        getJedis().zadd(otherSetKey,15,"kim");
        getJedis().zadd(otherSetKey,75,"alina");

        //求交集,结果保存到desSet 集合中
        String desSet ="destinationSet";
        Long zinterstore = getJedis().zinterstore(desSet, key, otherSetKey);
        ZParams zParams =new ZParams();
        Long zinterstore1 = getJedis().zinterstore(desSet, zParams, key, otherSetKey);

        //求并集 zunionstore destination numkeys key [key ...] [weights weight [weight ...]]
        Long zunionstore = getJedis().zunionstore(desSet, key, otherSetKey);
        

    }

四、sortlist(有序集合)的常见使用场景

  1.  用来排序,做各种花式排行榜 。利用可按照score指定方向排序,取Top N,倒数的数据
  2.  用来取值,利用zrange 指定获取某个范围内的不重复集合
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值