Redis ZADD NX 与 GT 怎么设计排行榜更新:同分、回退和幂等校验
来源:17golang原创
时间:2026-08-24 15:59:27 374浏览 收藏
排行榜更新最容易出错的地方,不是把分数写进 Redis,而是晚到消息、重复消息和同分记录一起出现时,旧分数会不会被覆盖。一个稳妥的做法是把“第一次写入”和“只接受更高分”拆成明确的更新策略:新成员用 NX,已有成员只允许升分则用 GT,再用成员字段处理稳定的同分排序。
ZADD NX只负责首次写入,适合防止重复初始化覆盖已有分数。ZADD GT只接受更高分,能挡住迟到的旧消息和重放数据。- 同分时 Redis 仍按成员字典序排列,业务需要稳定顺序时要把排序规则放进成员值。
- 生产更新要记录返回值和消息版本,读榜失败时再走可接受延迟的回退查询。
先把排行榜更新拆成三种结果
假设有一个赛季榜 leaderboard:season,成员是用户编号,分数是整数。一次更新到达时,至少要区分“成员不存在”“新分数更高”和“消息过期”三个结果。把它们混成无条件 ZADD key score member,看起来简单,实际上会让旧消息把新成绩改回去。
| 场景 | 推荐选项 | 接受条件 |
|---|---|---|
| 首次建立成员 | NX | 成员不存在才写入 |
| 只允许成绩上升 | GT | 新分数大于已有分数 |
| 允许纠错或回滚 | 普通写入 + 版本判断 | 业务版本确认后才覆盖 |

ZADD NX 适合防止初始化被重复消息覆盖
首次导入排行榜快照时,可以使用 NX。它的语义是“成员不存在才添加”,而不是“分数更大才更新”。因此同一个用户已经在榜上时,后来的初始化消息不会悄悄覆盖在线计算出的分数。
redis-cli ZADD leaderboard:season NX 128 user:42
redis-cli ZADD leaderboard:season NX 116 user:42
redis-cli ZSCORE leaderboard:season user:42
第一次命令返回 1,第二次返回 0,最后读取到的仍是 128。应用层不要只看命令是否抛错,返回值为零也应被记录为“成员已存在”,这样重放消息才有可观察性。
ZADD GT 能挡住迟到的旧分数
实时积分场景通常只允许分数上升,这时用 GT 比在客户端先查分再比较更稳。客户端先查再写会产生竞态:两个请求都读到 120,随后分别写入 128 和 125,后到的 125 可能把排行榜降回去。
redis-cli ZADD leaderboard:season GT CH 128 user:42
redis-cli ZADD leaderboard:season GT CH 125 user:42
redis-cli ZSCORE leaderboard:season user:42
在已经是 128 的情况下,第二条不会更新。CH 让返回值统计“分数确实变化的成员数”,便于把“命令执行成功”和“排行榜发生变化”分开记录。
同分排序不要交给偶然的成员字典序
Sorted Set 先按分数排序,分数相同的成员再按成员值的字典序排序。如果业务要求“先达到者优先”或“完成时间更早者优先”,直接把用户编号作为成员就不够用了。可以把不可变的时间序号编码进成员值,读取后再拆出用户编号;也可以留着 Redis 榜单当分数索引,额外用数据库记录同分时的对应时间。
这里要先把产品规则捋清楚:是要展示完全稳定的顺序,还是只需要按分数分组就行。别为了追求“看起来顺序稳定”临时修改用户编号,不然会影响后续删除、查询和跨榜单复用的逻辑。
允许回退时要增加版本边界
GT 适合“成绩只增不减”,不适合管理员纠错、撤销作弊分或重新计算历史成绩。需要降分时,建议给消息带上单调递增的 revision,由 Lua 原子地比较版本并更新分数,而不是把普通 ZADD 当成万能覆盖。
-- 伪代码:应用层先保证 revision 单调,再进入原子更新
if incoming_revision > stored_revision then
update_score_and_revision()
end
如果暂时没做版本字段,至少把纠错写入独立队列,人工确认后再执行覆盖操作,同时记录旧分数、新分数、操作者和修改原因。排行榜的可追溯性通常比少写几行代码更重要。

上线前用四个检查点验证幂等性
- 重复发送同一条首次消息,确认
NX不会改变已有分数。 - 先写入 160,再发送迟到的 150,确认
GT保持 160。 - 发送 172,确认榜单分数上升,并检查
CH的变化计数。 - 构造两个同分成员,确认展示层使用了明确的同分策略。
监控上建议至少保留“更新请求数、实际变更数、被条件拒绝数、版本冲突数”四类指标。拒绝数突然升高,大多说明消息延迟或者生产者重放,不是 Redis 本身不可用。
相关问题
ZADD NX 和 GT 可以同时使用吗?
可以组合使用,但要先确认语义完全贴合业务。组合后是“成员不存在时添加,成员已存在时只接受更高分”,适合只升不降的榜单;需要区分初始化和实时更新逻辑时,拆成两条命令更容易排查和观测。
为什么 GT 没有更新同分记录?
GT 只接受严格更高的分数,同分不会触发更新。如果同分需要改变顺序,应把排序依据设计进成员值或单独保存同分时间。
排行榜读不到时能不能直接查数据库?
可以把数据库作为回退兜底,但要接受随之而来的延迟和排序口径差异。回退返回的结果最好标记来源,同时不要把单次读失败误判成整个榜单为空。
总结
Redis 排行榜更新的关键不是记住几个参数,而是先写清楚“成员不存在时怎么办、旧分数到达时怎么办、同分时谁优先”。NX 负责初始化保护,GT 负责只升不降,版本字段负责可控纠错;把返回值、拒绝原因和回退来源记录下来,重复消息也能变成可验证的正常路径。
-
398 收藏
-
117 收藏
-
426 收藏
-
298 收藏
-
171 收藏
-
数据库 · Redis | 33分钟前 | Redis · 缓存 · 运维排查 · 过期策略 · PubSub · redis TTL 过期事件 keyspace notification notify-keyspace-events477 收藏
-
189 收藏
-
450 收藏
-
115 收藏
-
382 收藏
-
464 收藏
-
数据库 · Redis | 7小时前 | Redis · 消息队列 · 重试机制 · Stream · 消费组 · ACK · redis streams 重试 XREADGROUP XACK PEL pending339 收藏
-
数据库 · Redis | 18小时前 | Redis · 缓存 · 排行榜 · Sorted Set · 数据筛选 · redis 排行榜 BYSCORE Sorted Set ZRANGESTORE 窗口查询420 收藏
-
381 收藏
-
247 收藏
-
330 收藏
-
218 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习