ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis Zset 详解:有序集合原理、命令与实战场景

Redis Zset 详解:有序集合原理、命令与实战场景 搞 Redis 搞到第五篇终于轮到压轴的 Zset 了。如果你之前已经把 String、List、Hash、Set 都摸过一遍那 Zset 算是这五兄弟里最聪明的一个——它不是简单地存一堆值而是能让这些值自动排好序还能快速按名次或分数范围取数据。排行榜、热点榜单、延时队列、滑动窗口限流这些经典需求在 Redis 里基本都是靠 Zset 一把梭搞定。这篇我打算换个方式不按官方文档的条目硬抄而是从Zset 能解决什么痛点出发把原理、命令、场景、坑一次讲透。无论你是刚学 Redis 的新手还是工作中正准备用 Zset 换掉一段手写排序逻辑的老手这篇文章应该都能给你点实在的参考。1. 认识 Zset它到底解决了什么问题1.1 一句话理解 ZsetZset 的全称是 Sorted Set中文翻译通常叫有序集合。你可以把它理解成带了分数的 Set——每个成员member都是唯一的这一点跟 Set 完全一样但每个成员还必须关联一个分数scoreRedis 会根据这个 score 从小到大地自动维护顺序。注意这里的 score 不一定是整数可以是浮点数。成员的唯一性由 member 本身决定跟 score 无关。如果两次 ZADD 写入同一个 member后写入的 score 会覆盖前面的。你可以在脑子里建立一个画面一个班级里每个学生都有一个成绩Zset 就是这个按成绩升序排好队的花名册。你随时可以问张三排第几名前五名是谁60 到 80 分之间有哪些人它都能非常快地回答你。1.2 为什么非它不可在 Redis 五大数据类型里List 和 Set 各有各的短板而 Zset 恰好补上了排序这个空缺。先看 List。List 本身是有序的但这个顺序完全靠你维护在左边 push 的数据必须读的时候按同样顺序取你想让它按某个字段排序就麻烦了得先把数据全取出来再到程序里排一遍。当数据量上万这种取出再排的方式在性能上直接被打回原形。再看 Set。Set 适合做去重、交集并集运算但它最大的问题是没有顺序。你要实现积分排行榜把玩家 ID 放进去之后根本不知道谁分数高谁分数低还得另外维护一张分数表两个结构之间还要手动同步太容易出错。Zset 的设计初衷就是把唯一性约束和按分数排序这两件事在底层数据结构里一次性解决。你只管往里写数据Redis 帮你把排序、排名、区间查询都处理好了读取的时候直接拿结果不用在业务代码里再做任何排序动作。1.3 三个核心业务的直观对比为了让你更直观地感受这套组合能力我把五大数据类型里最容易混淆的 Set、List、Zset 放在一起对比能力维度SetListZset成员是否唯一是否是是否保持插入顺序否是否按 score 排序是否支持自动排序否否是按名次取数据不能可模拟但代价高原生支持按分数范围取数据不能不能原生支持典型场景标签、去重、好友关系消息队列、最新列表排行榜、延时队列、限流这张表背后的含义是如果你的需求里同时出现了去重和排序两个词那基本可以直接锁定 Zset不用再纠结其他结构。2. 核心命令速查与操作要点2.1 写入与删除类命令ZADD向 Zset 里添加元素每个元素必须带上自己的 scoreZADD leaderboard 100 player_1 ZADD leaderboard 200 player_2 150 player_3这里有个细节一条 ZADD 可以一次添加多个score member键值对批量操作的 RTT往返时延比循环单条添加低得多线上写代码时尽量用这种批量写法。ZINCRBY给某个成员的分数做增量操作ZINCRBY leaderboard 50 player_1这个命令有多重要实现排行榜时用户每赢一局就调一次 ZINCRBYRedis 内部会原子性地完成分数增加并重新排序不需要加锁不用担心并发覆盖。工作中实现积分累计、热度值变化强烈建议用 ZINCRBY 而不是先 ZSCORE 查出来再 ZADD 写回去后一种两步操作在高并发下会丢数据。ZREM按 member 删除ZREM leaderboard player_3ZREMRANGEBYRANK / ZREMRANGEBYSCORE按排名区间或分数区间删除元素。想清空排名后 100 的玩家ZREMRANGEBYRANK leaderboard 0 -101这里 -101 是倒数第 101 个位置配合 ZRANGE 和 ZCARD 可以灵活实现只保留前 100 名的裁剪效果。2.2 查询类命令ZRANGE / ZREVRANGE按排名范围取成员。ZRANGE 从低分到高分取ZREVRANGE 从高分到低分取# 取排名前 3分数最高 ZREVRANGE leaderboard 0 2 WITHSCORES # 取全部成员按分数升序 ZRANGE leaderboard 0 -1 WITHSCORESWITHSCORES 参数会把分数一起返回方便后续直接渲染成榜单项非常常用。ZRANGEBYSCORE按分数区间取成员这个命令是延时队列和滑动窗口场景的核心# 取 50 到 100 分之间的成员 ZRANGEBYSCORE leaderboard 50 100 WITHSCORES # 取 0 分到正无穷也就是当前分数大于等于 0 的所有成员 ZRANGEBYSCORE leaderboard 0 inf # 取负无穷到当前时间戳用来拉取到期任务 ZRANGEBYSCORE delay_queue -inf 1700000000分数区间的边界默认是闭区间如果想去掉某个端点用左括号(比如ZRANGEBYSCORE leaderboard (50 100表示不包括 50 分本身。ZRANK / ZREVRANK查成员的排名。ZRANK 返回升序排名ZREVRANK 返回降序排名名次从 0 开始ZRANK leaderboard player_2 ZREVRANK leaderboard player_2ZSCORE直接查某个成员的分数ZSCORE leaderboard player_2ZCARD / ZCOUNTZCARD 返回成员总数ZCOUNT 返回分数区间内的成员数量ZCARD leaderboard ZCOUNT leaderboard 60 100这两个命令在统计榜单总人数、统计某个分数段成绩分布时特别省事。2.3 集合运算类命令ZUNIONSTORE / ZINTERSTOREZset 也支持类似 Set 的并集和交集运算而且计算结果会存成一个新的 Zset# 把两个排行榜合并权重分别是 1 和 0.5 ZUNIONSTORE total_rank 2 week_rank month_rank WEIGHTS 1 0.5 AGGREGATE SUM # 取两个集合共同存在的成员用于筛选同时满足多个条件的用户 ZINTERSTORE common_user 2 group_a group_b权重参数 WEIGHTS 的含义是取并集/交集时对某个集合里所有成员的 score 先乘以权重再合并。AGGREGATE 默认是 SUM分数相加还可以指定 MIN 或者 MAX——这就非常适合做多维度取最小/最大值这类统计需求比如多个规则打分里取最高分。友情提醒ZUNIONSTORE 和 ZINTERSTORE 的结果集大小取决于集合中元素个数如果集合很大几十万以上建议在业务低峰期执行避免阻塞 Redis 单线程处理其他请求。2.4 命令时间复杂度速查上面这些命令的时间复杂度差异非常大我把高频命令整理成一张表写代码前先看一眼命令时间复杂度说明ZADDO(log N)N 是集合大小加一个元素要二分查找插入位置ZSCOREO(1)直接查哈希表ZRANK / ZREVRANKO(log N)跳表层序遍历过程中统计排名ZRANGE / ZREVRANGEO(log N M)M 是返回的元素个数分页取少量很划算ZRANGEBYSCOREO(log N M)类似 ZRANGEZCOUNTO(log N)直接拿区间的起止位置计算ZINCRBYO(log N)修改 score 后要移动元素位置ZREMO(log N)删除后调整跳表ZUNIONSTORE / ZINTERSTOREO(N) O(M * log N)涉及多集合遍历量大会拖累性能核心判断标准只要涉及按分数区间或排名区间操作Zset 都能在 O(log N) 级别搞定这也是它吊打一堆取出来再排序方案的根本原因。3. 底层原理skiplist 和 dict 的组合拳3.1 为什么不能简单用一个有序数组Zset 要求同时支持两种高效操作一是按分数快速定位区间二是按 member 快速查分数。如果只用有序数组按分数二分查找是没问题但插入和删除元素时要移动大量数据O(N) 的代价在数据量稍大的场景下根本扛不住。如果只用哈希表查询分数是 O(1) 没问题但按分数排序输出就彻底没戏了哈希表的顺序是随机的没法维护。官方最终给出的答案是Zset 底层同时维护了两个结构——一个哈希表dict存 member 到 score 的映射一个跳表skiplist存按 score 排序的有序结构。3.2 跳表是怎么工作的跳表本质上是一个多层级的有序链表。普通链表的劣势在于查找必须从头到尾逐个遍历。跳表的做法是在原始有序链表之上抽取一部分节点作为索引层索引层再往上抽一层形成多层结构。查找时从最高层开始每次尽量往右跳跳过头了就往下层走这样能快速缩小范围。我举个生活化的例子想象一本新华字典如果你只有一张按拼音排列的词条表查张这个字得从第一页翻到大概两三百页很慢。但现在你手里有几张页码索引卡第一张卡只记录每一百页的第一个字是什么第二张卡只记录每五十页的第一个字你就能先在索引卡上快速定位到张大致在哪个区间再下到详细页确认。跳表查找就是这种先跳大步再跳小步最后落到目标的思路。具体到 Redis 的实现里跳表节点会保存 member 和 score。当两个节点的 score 相同时再按 member 的字典序排序保证排序的稳定性。每一步查找的时间复杂度控制在 O(log N) 左右插入和删除也只需要局部调整指针代价同样是 O(log N)。3.3 哈希表在这套组合里的角色跳表负责按分数排序哈希表负责按 member 精确找。当你执行 ZSCORE 取某个成员的分数时Redis 不会去跳表里线性搜而是直接走哈希表 O(1) 出结果。当你要往跳表里插入一个 member 时也会先经过哈希表确认这个 member 是否已存在已存在的话就更新分数不存在就插入。这两套结构在代码里是同生共死的关系插入时两边都写删除时两边都删修改分数时哈希表更新映射跳表删除旧节点并重新插入新分数对应的位置。Redis 通过严格的事务性操作把两个结构绑在一起保证任何一个时刻两个结构反映的数据状态完全一致。3.4 内存优化从 ziplist 到 skiplist 的转换你可能会问既然跳表和哈希表这么强为什么不所有情况下都用答案是内存。跳表节点有多个 forward 指针哈希表也有额外的指针和元数据数据量小的时候这些开销纯属浪费。Redis 给了两个可控参数zset-max-ziplist-entries默认 128Zset 元素数量超过这个值从 ziplist 转为 skiplist。zset-max-ziplist-value默认 64如果某个 member 或者 score 的字符串长度超过这个值也会触发转换。ziplist 是一种紧凑的连续内存结构新增数据时可能触发内存重新分配和复制数据量小的时候效率尚可、内存占用极低但到大几千上万之后性能就会明显下降。所以 Redis 在小数据量时优先用 ziplist 省内存达到阈值后自动切到 skiplist 保证性能。这个转换是单向的一旦从 ziplist 变成 skiplist不会自动转回来。如果你自己搭 Redis数据量比较小的情况下可以把这两个参数改大一点进一步省内存。但如果预估数据量会快速增长建议保持默认值省得后面触发转换时那一下卡顿影响到线上请求。4. 记一次排行榜功能的上线全过程4.1 需求背景与方案选型我之前接了一个社区类 App 的需求做一个月度互动之星排行榜统计每个用户的获赞数、评论数、转发数按总得分降序排列并且要展示当前用户自己的排名。这个需求第一反应是查 MySQL 的ORDER BY但用户总量十几万每天又有几十万次互动事件每次都实时查库排序库根本扛不住而且上榜前 50 名还要给运营后台做配置加权。这时候我用 Redis Zset 来做实时的分数聚合 排序MySQL 只负责数据最终落库备份承担的压力小很多。4.2 数据结构设计我设置了两个 Zsetrank:user:month:202402记录每个用户的当月总互动分数。rank:user:month:202402:temp临时统计增量每小时跑一次批处理把增量合并进主榜单。key 里带上202402这个月份后缀是为了月底做榜单归档时非常方便直接把整个 key 改名或删除不用小心翼翼地逐条清理。score 的计算公式做了加权处理总得分 获赞数 * 1 评论数 * 2 转发数 * 3因为 Zset 的 score 只能存一个数字需要把三个维度的数据压缩成一个数值。具体做法是每当收到一条互动消息就更新用户在 temp Zset 里的分数比如收到一条评论就给这个用户的 score 加 2收到一条转发就加 3等批处理时间到了再通过 ZUNIONSTORE 把 temp 合并进主榜单。4.3 写入和查询的关键代码写入侧的逻辑大概长这样我用 Java 的 RedisTemplate 演示核心部分public void addScore(String userId, int score) { String tempKey rank:user:month:202402:temp; stringRedisTemplate.opsForZSet().incrementScore(tempKey, userId, score); }查询榜单前 50 名从高分到低分public ListUserRankVO top50() { String rankKey rank:user:month:202402; SetZSetOperations.TypedTupleString tuples stringRedisTemplate.opsForZSet().reverseRangeWithScores(rankKey, 0, 49); // 遍历 tuples 组装返回结果 }查当前用户自己的排名和分数public UserRankVO selfRank(String userId) { String rankKey rank:user:month:202402; Double score stringRedisTemplate.opsForZSet().score(rankKey, userId); Long rank stringRedisTemplate.opsForZSet().reverseRank(rankKey, userId); // 注意 reverseRank 返回从 0 开始的索引展示的时候要 1 }4.4 定时合并与过期策略临时增量 data 合并进主榜我用了一个 Spring Schedule 每 10 分钟执行一次public void mergeTempToMain() { String tempKey rank:user:month:202402:temp; String mainKey rank:user:month:202402; // 把 temp 集合里所有成员按分数累加到主集合里权重 1 表示原样合并 stringRedisTemplate.opsForZSet().unionAndStore(mainKey, tempKey, mainKey); // 合并完成后清空临时集合 stringRedisTemplate.delete(tempKey); }这里有个很关键的细节unionAndStore(mainKey, tempKey, mainKey)是允许目标 key 和源 key 相同的结果集就是主榜单里每个成员加上 temp 里同名的增量没有在 temp 里出现的保持原分数完美实现了增量合并。月度结束归档时再写一个任务把主榜单 key 改名成rank:user:month:202402:archive然后删掉临时 key进入下个周期的计算。4.5 上线后的性能表现上线后最高峰时期大概每秒有 2000 次左右 ZINCRBY 写入P99 耗时稳定在 1ms 以内。榜单查询因为有 Redis 层的缓存 Zset 的跳表结构即使是 ZRANGEBYSCORE 拉取全量榜单耗时也都在几毫秒级别。相比之前直接查 MySQL 动不动几百毫秒的排序查询至少提升了两个数量级。这一步方案演变其实很多团队都走过核心是写入时就把排序算好而不是查询时临时去算。Zset 本质上是以空间换时间的典型代表代价是内存里多存一份有序结构换来的是 O(log N) 级别的高效读写和排名查询。5. 实战场景延伸延时队列、限流与分布式锁排行榜是 Zset 最容易想到的场景但 Zset 的价值远不止于此。如果你接手一个中大型分布式系统在下面几个场景里用 Zset 能省掉一大半自研组件的功夫。5.1 基于 Zset 的延时队列延时队列的业务需求非常常见订单超时未支付自动关闭、外卖超时未接单自动退款、定时任务延迟执行。传统做法是每个任务启动一个定时器但任务量一大线程和内存开销都不小。Zset 的方案是把任务的执行时间戳当作 score任务内容作为 memberZADD delay_queue 1700000000 task_order_1001 ZADD delay_queue 1700003600 task_order_1002消费者进程每隔一段时间执行一次 ZRANGEBYSCORE把 score 小于当前时间的所有任务取出来处理// 伪代码 long now System.currentTimeMillis() / 1000; SetString tasks redis.opsForZSet().rangeByScore(delay_queue, 0, now); for (String task : tasks) { // 处理任务 process(task); // 处理成功后删除防止重复消费 redis.opsForZSet().remove(delay_queue, task); }这里有一个并发下的坑多个消费者同时扫描到同一批到期的任务可能会重复处理。稳妥做法是取出任务后先 ZREM 再执行或者用 ZREMRANGEBYSCORE 直接把窗口拉出来再执行这样每个任务只可能被一个消费者拿到。短时间内的重复扫描也不怕因为任务已经不在集合里了。5.2 滑动窗口限流限流最常见的有两种思路固定窗口计数和时间戳滑动窗口。固定窗口的边界处会有流量突刺问题而滑动窗口能更平滑地控制速率。Zset 实现滑动窗口限流的逻辑很直接把每次请求的时间戳作为 memberscore 也设为相同的时间戳public boolean isAllowed(String userId, int maxRequests, int windowSeconds) { String key rate:user: userId; long now System.currentTimeMillis(); long windowStart now - windowSeconds * 1000L; // 移除窗口外的记录 redis.opsForZSet().removeRangeByScore(key, 0, windowStart); // 统计当前窗口内的请求数 Long count redis.opsForZSet().zCard(key); if (count ! null count maxRequests) { return false; } // 加入当前请求 redis.opsForZSet().add(key, String.valueOf(now), now); return true; }这里 member 用时间戳字符串主要是为了保证同一毫秒内多个请求也能被记录member 必须唯一。因为每次限流判断都会先把过期窗口清理掉所以这个 Zset 不会无限增长一般超过窗口时间就会自动被 remove 掉。相比 Redisson 的 RateLimiter用 Zset 自己实现的好处是线程数、客户端语言都不受限制逻辑完全透明可控。5.3 用 Zset 实现公平分布式锁普通的 Redis 分布式锁SETNX 过期时间在高并发场景下容易遇到锁被其他线程误删的坑而且不保证公平性——后来者可能永远抢不到锁。Zset 可以通过把请求排队编号作为 score 来实现一把公平锁public boolean tryFairLock(String lockKey, String requestId, int timeoutSeconds) { String queueKey fairlock:queue: lockKey; long requestTime System.nanoTime(); // 用纳秒时间戳作为排队顺序 // 加入排队队列member 是 requestId redis.opsForZSet().add(queueKey, requestId, requestTime); // 获取自己在队列中的排名 Long rank redis.opsForZSet().rank(queueKey, requestId); if (rank null) { return false; } // 自己排在第一位说明可以拿到锁 if (rank 0) { redis.opsForValue().set(fairlock:holder: lockKey, requestId, timeoutSeconds, TimeUnit.SECONDS); return true; } // 没排到第一位等待一段时间后再来查 return false; }这种实现方式的好处是多个线程不会同时疯抢锁而是按照先来后到的顺序依次拿锁非常公平。代价是需要额外的轮询和排队逻辑适合对公平性要求高的业务场景比如定期批量任务中的资源分配。如果只是普通互斥需求SETNX 那把简单锁就够用了。6. 常见问题与排坑记录6.1 大数据量下的分页性能问题很多新手用 Zset 做排行榜分页时习惯直接把 offset 设得很大比如第 100 万条数据之后的分页。Redis 的 ZRANGE 在取尾部数据时需要从跳表头部一路往下走相当于 O(offset)当 offset 很大时性能会急剧恶化。我实测过一个 500 万成员的榜单直接取第 400 万到 400 万零 5 名耗时比取前 10 名慢了差不多两个数量级。优化思路有两个限制最大分页深度只允许用户翻到前 1000 名1000 名之后不提供分页用按分数区间拉取替代。如果必须支持深度分页考虑用上一次的最后一名分数作为游标比如ZRANGEBYSCORE rank (lastScore inf LIMIT 0 10用游标滑动的思路替代 offset 跳转性能稳定在 O(log N M)。6.2 score 相同导致排名不稳定排行榜必须处理同分的情况。如果直接用 ZRANK 查排名Redis 在跳表内部会按 member 字典序排在两个 member 分数相同的时候谁的字典序小谁排在前面这对业务来说是不可控的。常见解法是业务自己扩展排名规则只把 Zset 的排名当作并列排名来用相同分数显示同一个名次比如前两名都是 100 分就都显示第 1 名第三名是 90 分就显示第 3 名。另一种做法是改造 score把时间因素也编码进 score 里比如score 总分数 1 - 时间戳的归一化值这样分数高的一定排在前面分数相同的按时间排序但这个方案会让 score 失去直观性改排行规则时要谨慎。6.3 Zset 作为缓存 持久化的双层结构使用时怎么保证一致性很多团队用 Zset 做热点排行榜同时又要求数据不能丢。Redis 毕竟持久化策略有 RDB 和 AOF 两种如果配置不当重启后排行榜可能回滚到很久以前的状态。我的经验是对于排行榜这种对实时性要求高、但对个别数据丢失容忍度较高的场景开启 AOF 的appendfsync everysec足够最多丢 1 秒的数据影响可接受。但如果 Zset 同时用来做延时队列任务一旦丢失就会造成业务事故建议开启appendfsync always或者走业务自己保障消息不丢的机制——把任务写入数据库Zset 只作为加速查询的索引处理成功后再删。6.4 大 key 和热 key 的治理Zset 如果被塞得特别大比如几百万个成员同时又承载高频读写就很容易成为 Redis 里的热点 key单个 key 的读写压力都集中到一个分片节点上这个节点会成为性能瓶颈。解决办法是分片把一个大 Zset 按业务维度拆成多个 key比如按业务线分、按用户 ID 哈希分读的时候并发查多个 key 再合并结果。另外如果你发现线上 Redis 的used_memory增长很快排查完大 key 之后别忘了看这几个参数zset-max-ziplist-entries和zset-max-ziplist-value是否合理。如果数据量不大却用了 skiplist内存浪费可能超出预期调整配置后能直接省出一大块内存。6.5 Zset 里 member 的设计规范隐蔽的坑Zset 的 member 有一个容易忽略的约束member 在整个集合中是唯一的。如果你用字符串拼接业务 ID一定要自己保证拼接结果的唯一性。我踩过一个真实案例运营后台配置全量用户排行榜member 直接用用户 ID 拼日期本意是每个人每天一条。但运营改了需求后要求只看当月累计我就改成每天写入时直接 ZINCRBY结果某些老用户的历史记录因为 member 和当天一样被覆盖了排行榜瞬间少了很多人。这个问题的根因是 member 设计时没有把统计周期考虑进去。避免方法很简单member 里尽量带上归属粒度的唯一标识比如month:202402:user:1001这样后续扩展统计范围时只需要改 key 前缀不会污染已有数据。7. 再给你一份 Zset 面试高频题清单如果你正在准备面试或者要带一个刚接触 Redis 的同事这几个问题基本是绕不开的。我顺手把参考答案的核心也写一下当作速查用。Q1Zset 底层用的是跳表为什么不用红黑树红黑树在内存里也是 O(log N) 级别的平衡树但实现复杂度比跳表高很多。跳表结构简单容易通过调整概率因子来控制层数范围查询比如 ZRANGEBYSCORE在跳表里只需要找到起始节点再沿链表往后遍历非常自然红黑树的区间查询则要中序遍历和回溯实现复杂多了。跳表还天生利于并发——查找的时候不需要加锁插入时只影响局部节点。对 Redis 这种追求简单和极致性能的场景跳表明显更合适。Q2Zset 为什么还要搭配一个 dict跳表负责排序和区间操作dict 负责 O(1) 的精确查找。两者结合Zset 才能在按范围查和按 member 查两个维度都保持高效。如果没有 dictZSCORE 就退化成一个 O(log N) 的搜索反过来如果没有跳表ZRANK 就无法实现。它们互相补齐缺一不可。Q3Zset 的 ziplist 和 skiplist 的转换条件是什么ziplist 转换为 skiplist 的条件有两个满足任一就会触发一是元素数量超过zset-max-ziplist-entries默认 128二是新增元素的 member 长度或 score 长度超过zset-max-ziplist-value默认 64。转换过程会阻塞 Redis大数据量下建议在业务低峰期让数据量缓慢增长避免一次性大批量写入触发集中转换。Q4如何用 Zset 实现一个支持分页 总页数的排行榜总页数用 ZCARD 除以每页大小向上取整即可分页则用 ZREVRANGE 加 offset 和 limit。如果数据量很大可以用上一页最后一名分数做游标滑动避免大 offset 导致性能劣化。Q5Zset 作为延时队列时怎么避免任务重复消费核心是先移除再执行或者利用 Redis 的单线程特性保证 ZREMRANGEBYSCORE 的原子性。消费者把到期的任务一次性 ZREMRANGEBYSCORE 拉出来再从结果集合里逐个执行。只要移除成功其他消费者就不可能再次拿到这批任务。8. 关于 Zset 的几个运维经验文章最后这部分我写点偏运维向的经验。很多人开发时把 Zset 用得飞起真到线上出了问题就开始抓瞎这里提供三个方向。8.1 如何快速定位某个 Zset 是否是大 key线上没必要全靠猜Redis 自带的命令就能查# 查看当前库中所有 key 的大小分布 redis-cli --bigkeys--bigkeys扫描会把每一种数据类型的最大 key 和占比都列出来跑一遍就能看出哪几个 Zset 吞内存。平时做容量预估也可以用DEBUG OBJECT key查看编码方式和序列化长度。8.2 持久化配置对 Zset 的影响Zset 写操作特别频繁时AOF 的 fsync 策略对 Redis 写吞吐影响很大。如果业务对数据丢失容忍度高建议appendfsync everysec如果数据绝不能丢必须appendfsync always但性能会下降约一个量级。RDB 快照在 Zset 数据量特别大的场景生成快照和恢复重载都比较耗时需要重点监控rdb_bgsave_in_progress指标。8.3 集群模式下 Zset 的 key 分布策略Redis Cluster 分片是按 key 哈希的如果某个 Zset 的 key 对应的是热点业务比如全站总榜所有读写都会打到同一个分片上造成倾斜。我的建议是能拆就拆比如按业务线拆不能拆的话再考虑把榜单做成多级聚合先算子榜单再合并成总榜这样既能提高并发度也能减少单 key 压力。Zset 是我个人认为 Redis 里最简单又能最出效果的数据结构——它不像 Hash 一样需要你精心设计字段结构也不像 List 一样需要小心维护顺序。只要把 member 和 score 的模型想清楚很多排序类需求都能一行命令解决。如果你之前一直写的都是ORDER BY然后内存排序那我强烈建议下次遇到类似需求时先停下来想想这个东西放到 Zset 里是不是更优雅
返回列表