【Redis从入门到精通】第22篇:Set对象——intset和hashtable的混搭艺术

发布时间:2026/6/1 12:22:00

【Redis从入门到精通】第22篇:Set对象——intset和hashtable的混搭艺术 上一篇【第21篇】Hash对象——ziplist和hashtable的双重人格下一篇【第23篇】ZSet对象——ziplist和skiplist的完美组合如果Redis数据结构有鄙视链那ZSetSorted Set有序集合一定是站在顶端的那位。它是所有基础类型中最重的一个——底层结构复杂、能力强大、实现优雅。一个ZSet对象同时集成了ziplist紧凑存储、skiplist高效范围查询和hashtableO(1)点查询三种数据结构的优点堪称Redis设计哲学的集大成者。一、ZSet对象概览一个类型两种编码ZSet和其他Redis对象一样干着见人下菜碟的活。数据小的时候用ziplist数据大了用skiplist。但和其他类型不同的是skiplist编码下的ZSet内部还绑着一个hashtable形成了一种双核心架构。先来一张总览表特性ziplist编码skiplist编码底层结构单个ziplistskiplist hashtable双结构memberscore存储交替存储在ziplist中skiplist负责排序hashtable负责点查询排序方式物理顺序按score升序跳表逻辑顺序O(logN)内存占用紧凑省内存三个结构并存内存开销大ZRANK查询O(N)需遍历计数O(logN)跳表直接定位ZSCORE查询O(N)需遍历O(1)hashtable直接定位二、ziplist编码挤得整整齐齐当ZSet元素不多时Redis会把所有member和score交替存放在一个ziplist中而且按score升序排列。ziplist编码下的ZSet存储结构 ZSet: {(张三, 89), (李四, 92), (王五, 85), (赵六, 96)} ziplist实际存储按score升序排列 ---------------------------------------------------------------- | 王五 | 85 | 张三 | 89 | 李四 | 92 | 赵六 | 96 | ---------------------------------------------------------------- member1 score1 member2 score2 member3 score3 member4 score4 score升序 → 85 89 92 96 关键规则 1. member和score交替存储 2. 整体按score从小到大排序 3. score相同的按member的字典序排序这个排列方式决定了在ziplist编码下ZRANGE按排名查非常低效——因为它需要从头遍历到目标的rank位置才能找到数据。但是在数据量小的情况下默认128个以内N这么小遍历的开销几乎感觉不到。来实操一下ziplist编码的ZSet# 创建小规模ZSet127.0.0.1:6379ZADD game:rank89张三92李四85王五96赵六(integer)4# 查看编码127.0.0.1:6379OBJECT ENCODING game:rankziplist# 按排名查看——正序低分到高分127.0.0.1:6379ZRANGE game:rank0-1WITHSCORES1)\xe7\x8e\x8b\xe4\xba\x94# 王五2)853)\xe5\xbc\xa0\xe4\xb8\x89# 张三4)895)\xe6\x9d\x8e\xe5\x9b\x9b# 李四6)927)\xe8\xb5\xb5\xe5\x85\xad# 赵六8)96# 查看张三的排名127.0.0.1:6379ZRANK game:rank张三(integer)1# 第1名从0开始三、skiplist编码双核驱动的强大引擎当ZSet的数据量上去了ziplist的遍历方式就力不从心了。这时Redis搬出skiplist编码的大杀器——一个同时拥有跳表和哈希表的双核架构。为什么需要两个结构这是很多初学者困惑的地方为什么skiplist不够用还要加一个hashtable答案在于两者各有所长互相补充skiplist编码下的ZSet内部结构 zset ------------- | dict (*) | | zsl (*) | ------------- | | v v hashtable skiplist (点查询O(1)) (范围查询O(logN)) ------------------ ------------------------------- | dict (hashtable)| | skiplist (跳表排序) | | | | | | key value | | L3: [头]-----------------[尾]| | 张三 89 | | | | | 李四 92 | | L2: [头]---[85]-----[92]--| | 王五 85 | | | | | | | 赵六 96 | | L1: [头]-[85]-[89]-[92]--| | | | | | | | | ------------------ | L0: [头]-[85]-[89]-[92]-[96]--[尾] | 王五 张三 李四 赵六 ------------------------------- 工作分工 - 查分数 (ZSCORE): dict → O(1) - 查排名 (ZRANK): skiplist → O(logN) - 范围查询 (ZRANGEBYSCORE): skiplist → O(logN M) - 新增元素 (ZADD): dict插入 skiplist插入 → O(logN)一句话总结这个设计哲学hashtable负责这个人的分数是多少skiplist负责分数在80-90之间的都有谁。用C代码看看这个结构体// Redis源码中的zset结构体typedefstructzset{dict*dict;// 字典keymembervaluescorezskiplist*zsl;// 跳表按score排序}zset;实际操作演示# 创建大规模ZSet自动用skiplist编码127.0.0.1:6379EVAL for i1,200 do redis.call(ZADD, big:zset, i, member..i) end 0127.0.0.1:6379OBJECT ENCODING big:zsetskiplist# ZSCORE——走hashtableO(1)127.0.0.1:6379ZSCORE big:zset member150150# ZRANK——走skiplistO(logN)127.0.0.1:6379ZRANK big:zset member150(integer)149# ZRANGEBYSCORE——走skiplist范围查询O(logNM)127.0.0.1:6379ZRANGEBYSCORE big:zset4050WITHSCORES1)member402)403)member414)41...踩坑提示skiplist编码下hashtable和skiplist中的数据是冗余重复的——同一个member在两个结构中各存了一份。这也解释了为什么ZSet是Redis中最吃内存的数据类型——你存入一个memberscore实际在内存中存了两份dict存一份skiplist存一份。四、编码转换什么时候放大招ZSet的编码转换规则和Hash类似由两个配置参数控制配置参数含义默认值zset-max-ziplist-entriesziplist最大元素数量128zset-max-ziplist-value单个member最大长度64字节需要注意的是这里判断的是member的长度不是score。score永远是数字double类型和value长度无关。# 演示编码转换127.0.0.1:6379CONFIG SET zset-max-ziplist-entries3OK# 3个元素——ziplist127.0.0.1:6379ZADD test:zset10a20b30c(integer)3127.0.0.1:6379OBJECT ENCODING test:zsetziplist# 第4个——触发转换127.0.0.1:6379ZADD test:zset40d(integer)1127.0.0.1:6379OBJECT ENCODING test:zsetskiplist同样如果member长度超过了64字节即使元素很少也会触发转换127.0.0.1:6379CONFIG SET zset-max-ziplist-entries512OK127.0.0.1:6379CONFIG SET zset-max-ziplist-value8OK127.0.0.1:6379ZADD test:zset210short(integer)1127.0.0.1:6379OBJECT ENCODING test:zset2ziplist# member长度超过8——触发转换127.0.0.1:6379ZADD test:zset220this_is_a_long_member_name(integer)1127.0.0.1:6379OBJECT ENCODING test:zset2skiplist踩坑提示ZSet的编码转换成本比Hash更高——因为它不仅要构建skiplist还要同时构建hashtable。大数据量下的转换会瞬间吃满CPU如果是在生产高峰期触发真的会让人血压飙升。五、核心命令的两种编码实现对比同样的命令在不同编码下走的是完全不同的代码路径命令ziplist实现skiplist实现ZADD遍历找到插入位置可能需要移动后续元素 O(N)跳表插入字典插入 O(logN)ZSCORE遍历找到member读取下一个entry的score O(N)从dict中直接获取 O(1)ZRANK从头遍历到找到member计步数 O(N)跳表查找span累加 O(logN)ZRANGE从指定位置起取M个 O(N)跳表定位起点然后沿L0层取M个 O(logNM)ZRANGEBYSCORE找到第一个≥min的元素遍历到max O(N)跳表定位min沿L0取到max O(logNM)ZREM遍历找到member删除并整理 O(N)dict删除跳表删除 O(logN)ZCOUNT遍历计数 O(N)跳表定位后计算 span 差值 O(logN)可以看到ziplist在大多数写操作上都吃亏。这就是为什么大数据量下一定要让ZSet用skiplist编码。六、排行榜实战ZSet的主战场排行榜是ZSet最经典的应用场景。下面我们来搭建一个完整的游戏得分排行榜# 1. 初始化玩家数据ZADD game:leaderboard0player:10010player:10020player:1003ZADD game:leaderboard0player:10040player:1005# 2. 玩家得分增加ZINCRBY game:leaderboard150player:1001ZINCRBY game:leaderboard200player:1003ZINCRBY game:leaderboard88player:1002ZINCRBY game:leaderboard300player:1004ZINCRBY game:leaderboard45player:1005# 3. 查看排行榜Top 3降序——高分在前127.0.0.1:6379ZREVRANGE game:leaderboard02WITHSCORES1)player:10042)3003)player:10034)2005)player:10016)150# 4. 查看某个玩家的排名和分数127.0.0.1:6379ZSCORE game:leaderboardplayer:100288127.0.0.1:6379ZREVRANK game:leaderboardplayer:1002(integer)3# 降序第3名# 5. 查看某个玩家附近的名次前一名和后一名127.0.0.1:6379ZREVRANK game:leaderboardplayer:1003(integer)1# player:1003排第1# 查看他后面的人127.0.0.1:6379ZREVRANGE game:leaderboard23WITHSCORES1)player:10012)1503)player:10024)88用ASCII图把排行榜的结构画出来游戏得分排行榜按score降序 排名 玩家ID 得分 #1 player:1004 300 ████████████████████████████ #2 player:1003 200 ██████████████████ #3 player:1001 150 ██████████████ #4 player:1002 88 ████████ #5 player:1005 45 ████ ZREVRANGE 0 2 WITHSCORES → 返回前3名 ZREVRANK player:1002 → 3 (降序排名0-based) ZSCORE player:1002 → 88ZINCRBY的妙用在排行榜场景中ZINCRBY是最重要的命令——它增量更新分数不需要先读后写保证了原子性# 玩家完成一局游戏获得新分数# ✅ 原子操作——不需要 GET → → SET127.0.0.1:6379ZINCRBY game:leaderboard50player:1001200# ❌ 如果用String需要这个步骤# GET score:player:1001 → 150# 计算: 150 50 200# SET score:player:1001 200# 中间可能被其他请求插队造成数据不一致七、范围查询的边界值技巧使用ZRANGEBYSCORE和ZREVRANGEBYSCORE时括号的用法很容易搞混。下面用一张表说清楚# 基础语法回顾# ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]# 默认闭区间[min, max]# 左开(min 右开(max# 列出score在80到95之间的含80和95127.0.0.1:6379ZRANGEBYSCORE game:rank8095WITHSCORES# 列出score大于80且小于等于95的不含80127.0.0.1:6379ZRANGEBYSCORE game:rank(8095WITHSCORES# 列出score大于等于80且小于95的不含95127.0.0.1:6379ZRANGEBYSCORE game:rank80(95WITHSCORES# 列出所有score大于80的用inf表示正无穷127.0.0.1:6379ZRANGEBYSCORE game:rank(80inf WITHSCORES# 列出所有score小于等于95的用-inf表示负无穷127.0.0.1:6379ZRANGEBYSCORE game:rank-inf95WITHSCORES边界符号速查表写法含义示例min max闭区间 [min, max]80 100→ score∈[80,100](min maxmin开区间 (min, max](80 100→ score∈(80,100]min (maxmax开区间 [min, max)80 (100→ score∈[80,100)-inf inf全区间返回所有元素(min inf大于min(0 inf→ 所有正分八、Redis 7.0的新玩具ZPOPMIN/ZPOPMAXRedis 7.0带来了一些很有用的ZSet新命令让队列场景的实现变得更优雅ZPOPMIN / ZPOPMAX弹出最小/最大元素# ZPOPMIN弹出score最小的元素和SPOP类似但是按score顺序弹出127.0.0.1:6379ZADD tasks:queue1task12task23task34task4(integer)4# 弹出分数最低的2个任务优先级最高的127.0.0.1:6379ZPOPMIN tasks:queue21)task12)13)task24)2# 剩下分数较高的任务127.0.0.1:6379ZRANGE tasks:queue0-1WITHSCORES1)task32)33)task44)4# ZPOPMAX弹出分数最高的127.0.0.1:6379ZPOPMAX tasks:queue11)task42)4BZPOPMIN / BZPOPMAX阻塞式弹出# 阻塞等待如果ZSet为空等待最多10秒# BZPOPMIN key timeout127.0.0.1:6379BZPOPMIN tasks:queue101)tasks:queue2)task33)3# 这个命令对任务队列场景特别友好# 替代了以前用BRPOPLPUSH实现延迟队列的复杂操作有了这些命令用ZSet实现优先级队列变得特别简洁ZSet作为优先级队列 生产者 消费者 ------------------ ------------------ | ZADD queue 1 t1 | → | BZPOPMIN queue 0 | | ZADD queue 5 t2 | | 弹出 t1 (优先级1) | | ZADD queue 3 t3 | | BZPOPMIN queue 0 | ------------------ | 弹出 t3 (优先级3) | ------------------ ZSet内部: ------------------ | t1(1) t3(3) t2(5)| ------------------ 按score升序低分优先处理踩坑提示ZPOPMIN和ZPOPMAX在Redis 5.0才加入BZPOPMIN和BZPOPMAX也是5.0引入的。如果你的Redis版本太低可以用ZRANGE ZREM的组合非原子或者升级到新版本。小结ZSet是Redis中最复杂也最强大的数据结构它用精巧的设计同时解决了三个问题排序、范围查询和点查询。核心要点回顾数据小的用ziplist——省内存但查询需要遍历数据大的用skiplist——本质是skiplist hashtable双结构排名查O(logN)、分数查O(1)skiplist和hashtable数据冗余——空间换时间一个负责排序一个负责点查排行榜是ZSet的天然主场——ZADD/ZINCRBY/ZREVRANGE/ZRANK组合拳一气呵成范围查询注意括号——(表示开区间-inf/inf表示无界Redis 7.0新增ZPOPMIN/ZPOPMAX——让ZSet做优先级队列更加顺手下一篇我们将从调优的角度全面审视Redis的内存使用——OBJECT ENCODING怎么玩、内存碎片怎么治、大Key怎么找、8种淘汰策略怎么选……敬请期待《Redis内存优化完全指南》上一篇【第21篇】Hash对象——ziplist和hashtable的双重人格下一篇【第23篇】ZSet对象——ziplist和skiplist的完美组合

相关新闻