Redis 源码分析有序集合对象(z_zset)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis 源码分析有序集合对象(z_zset)

数据结构


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


两种实现方式


1、ziplist 第一个节点保存元素的成员,而第二个节点则保存元素的分值。压缩列表内的集合元素按分支从小到大排序,分值小的元素被放置在靠近表头的方向,分值较大的被放置在靠近表尾的方向。


2、 skiplist实际上,使用 zset 结构对字典和跳跃表进行封装。zset 结构中的 zsl 跳跃表分支按照从小到大保存了所有元素。通过这个跳跃表,程序可以对有序集合进行范围操作,比如 zrank , zrange 等命令都是基于跳跃表来实现的。


ziplist 插入数据


/* Insert (element,score) pair in listpack. 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 = lpSeek(zl,0), *sptr;
    double s;
    // 排序的过程
    while (eptr != NULL) {
        sptr = lpNext(zl,eptr);
        serverAssert(sptr != NULL);
        s = zzlGetScore(sptr);
        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 = lpNext(zl,sptr);
    }
    /* Push on tail of list when it was not yet inserted. */
    if (eptr == NULL)
        zl = zzlInsertAt(zl,NULL,ele,score);
    return zl;
}


ziplist 压缩列表切换为跳跃表


/* Convert the sorted set object into a listpack if it is not already a listpack
 * and if the number of elements and the maximum element size and total elements size
 * are within the expected ranges. */
void zsetConvertToListpackIfNeeded(robj *zobj, size_t maxelelen, size_t totelelen) {
    if (zobj->encoding == OBJ_ENCODING_LISTPACK) return;
    zset *zset = zobj->ptr;
    if (zset->zsl->length <= server.zset_max_listpack_entries &&
        maxelelen <= server.zset_max_listpack_value &&
        lpSafeToAdd(NULL, totelelen))
    {
        zsetConvert(zobj,OBJ_ENCODING_LISTPACK);
    }
}


image.png


注意点


在 skiplist 的基础上,还需要创建 dict 的原因是当需要获取某个元素的 score 时,skiplist 的时候复杂度为 O(logN),而 dict 时间复杂度为 O(1) , 见(zsetAdd)。需要特别主要的是底层为 ziplist 时,该操作时间复杂度为 O(n)


int zsetScore(robj *zobj, sds member, double *score) {
    if (!zobj || !member) return C_ERR;
    if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
        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);
        if (de == NULL) return C_ERR;
        *score = *(double*)dictGetVal(de);
    } else {
        serverPanic("Unknown sorted set encoding");
    }
    return C_OK;
}
unsigned char *zzlFind(unsigned char *lp, sds ele, double *score) {
    unsigned char *eptr, *sptr;
    if ((eptr = lpFirst(lp)) == NULL) return NULL;
    eptr = lpFind(lp, eptr, (unsigned char*)ele, sdslen(ele), 1);
    if (eptr) {
        sptr = lpNext(lp,eptr);
        serverAssert(sptr != NULL);
        /* Matching element, pull out score. */
        if (score != NULL) *score = zzlGetScore(sptr);
        return eptr;
    }
    return NULL;
}


  • zkiplist 和 dict 共享元素和分值(指针复制)。


  • 由 zkiplist 转 skiplist 的操作是不可逆的


  • 两个参数 【zset-max-ziplist-entries】和 【zset-max-ziplist-value】可以在配置文件中修改


  • zset 也不允许重复


使用场景


  • 优先队列


  • 排行榜系统:主要是视频网站需要对用户上传的视频做排行榜,榜单的维度坑是多个方面的:按照时间、按照播放量、按照获取的赞排序


  • 添加用户赞数


zadd user:ranking:2022_02_20 3 mike


  • 增加点赞


zincrby user:ranking:2020_02_20 1 mike


  • 取消点赞


zrem user:ranking:2020_02_20 mike


  • 暂时获取点赞数最多的10 个用户


zrevrang user:rangking:2020_02_20 0 9


  • 显示用户信息以及用户分数和排名


hgetall user:info:tom
zscore user:rangking:2020_02_20 mike
zrank user:rangking:2020_02_20 mike


常见操作


image.png


Redis 对象总结


数据类型 使用场景 备注
字符串(string) 缓存;计数器;分布式锁 简单型的,比如 set strnum studentinfo. 计数器如限流
列表(list) lpush + lpop = Stack (栈) lpush + rpop = Queue (队列) lpush + ltrim = Capped Collection (有限集合) lpush + brpop = Message Queue (消息队列) 如阻塞队列,关注列表
哈希(hash) 对象属性(尤其不定长) 如缓存 studentinfo hmset setnum setnum 1 stuname dinghaijun age 33
集合(set) 适用于社交场景/推荐场景 点赞、粉丝、共同爱好/喜好、推送
有序集合(zset) 排行榜;优先队列;缓存相关的元数据(比如按照排序的挑战)


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 NoSQL Java
【Redis系列】那有序集合为什么要同时使用字典和跳跃表
面试官问:那有序集合为什么要同时使用字典和跳跃表来实现?我:这个设计主要是考虑了性能因素。1. 如果单纯使用字典,查询的效率很高是O(1),但执行类似ZRANGE、ZRNK时,排序性能低。每次排序需要在内存上对字典进行排序一次,同时消耗了额外的O(n)内存空间
28 1
【Redis系列】那有序集合为什么要同时使用字典和跳跃表
|
5月前
|
存储 NoSQL API
【Redis 系列】redis 学习四,set 集合,hash 哈希,zset 有序集合初步认知
【Redis 系列】redis 学习四,set 集合,hash 哈希,zset 有序集合初步认知
|
3月前
|
存储 NoSQL 网络协议
redis主从同步对象模型学习笔记
redis主从同步对象模型学习笔记
44 0
|
2月前
|
存储 NoSQL Redis
Redis淘汰策略、持久化、主从同步与对象模型
Redis淘汰策略、持久化、主从同步与对象模型
90 0
|
3月前
|
缓存 NoSQL Java
Redis实现商品信息对象缓存
Redis实现商品信息对象缓存
63 0
|
3月前
|
存储 NoSQL Redis
redis主从同步与对象模型
redis主从同步与对象模型
15 0
|
3月前
|
NoSQL 算法 Redis
Redis系列-11.RedLock算法和底层源码分析
Redis系列-11.RedLock算法和底层源码分析
43 0
|
3月前
|
存储 NoSQL 关系型数据库
Redis持久化策略AOF、RDB详解及源码分析
Redis持久化策略AOF、RDB详解及源码分析
|
3月前
|
NoSQL Unix Redis
Redis对象及redisObject源码解析
Redis对象及redisObject源码解析
|
3月前
|
存储 缓存 NoSQL
Redis主从同步与对象模型
Redis主从同步与对象模型
98 0

热门文章

最新文章