登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Sorted Set 排行榜如何处理同分、翻页与历史榜单

来源:17golang原创

时间:2026-10-07 09:06:52 189浏览 收藏

我早期做活动榜时,直接把 ZREVRANK + 1 当成用户名次,直到产品问“两个同分的人为什么一个第 2、一个第 3”。这不是 Redis 排错了,而是我们把“集合中的确定位置”误当成“业务展示名次”。更稳妥的规则是:Sorted Set 负责唯一成员、分数和稳定顺序;同分名次、翻页一致性与历史周期由业务协议补齐。

Redis 官方文档:https://redis.io/docs/latest/develop/data-types/sorted-sets/

  • 同分成员在 Redis 内仍按成员字符串排序,逆序查询时使用逆字典序。
  • 实时榜可以按位置分页,但必须承认更新会让页边界移动。
  • 历史榜最好从一开始就写入周期键,而不是结束时临时复制。

旧规则的问题:位置不等于并列名次

Sorted Set 的成员唯一,每个成员关联一个浮点分数。分数不同就按分数排序;分数相同则按成员字符串的字典序决定先后。ZRANGE ... REV 会把高分放前面,并反转同分成员的字典序。因此 Redis 能给同分用户不同的零基位置,但它并不知道产品想显示“1、2、2、4”还是“1、2、2、3”。

# 两名用户同为 980 分,Redis 仍会为成员保留确定顺序
redis-cli ZADD 'lb:{game}:2026-W41' 1050 'u:1000' 980 'u:1001' 980 'u:1002' 910 'u:1003'

# 取高分在前的榜单;同分顺序由成员字符串决定
redis-cli ZRANGE 'lb:{game}:2026-W41' 0 -1 REV WITHSCORES

# 返回的是零基位置,不是“同分同名次”的业务排名
redis-cli ZREVRANK 'lb:{game}:2026-W41' 'u:1001'
Redis Sorted Set 同分顺序与业务并列名次关系图
图1:同分处理的静态结构图。Sorted Set 用 score 与成员字符串确定存储位置,ZREVRANK 读取位置;业务层用 ZCOUNT 统计更高分成员,再生成并列竞赛名次。此图不是运行截图。

新规则:用更高分人数计算竞赛排名

如果展示规则是竞赛排名“1、2、2、4”,某成员的名次应等于“严格高于它的成员数 + 1”。ZCOUNT 的边界默认包含端点,给分数加 ( 前缀即可排除当前分数。下面 980 分的两名用户都只看到一名更高分用户,因此都显示第 2 名。

# 统计严格高于 980 分的成员;(980 表示不包含 980
redis-cli ZCOUNT 'lb:{game}:2026-W41' '(980' '+inf'

# 应用层把返回值加 1,得到并列竞赛名次
# 若一页包含多个分数,可按不同分数去重后用 pipeline 批量查询

我不建议把时间戳硬塞进小数位来“打破同分”。那会悄悄改变分数语义,还把展示规则绑死在编码技巧里。若业务确实需要先到先得,就把时间规则明确成产品规则,并单独设计可解释的复合排序数据,而不是假装大家仍然同分。

翻页的新语义:实时榜允许变化,但要可检测

按位置分页最直接:第 1 页取 0–19,第 2 页取 20–39。官方文档也提醒,大偏移量需要先遍历前面的元素,代价可能上升到 O(N)。更重要的是,用户翻页期间只要有人加分,页边界就可能移动,出现重复或遗漏。对实时榜,我会返回一个版本号;下一页请求若发现版本变化,就提示刷新或从第一页重新开始。

# 分数和榜单版本放在同一连接的事务中更新
redis-cli 

严格连续、不重不漏的翻页不适合在持续写入的同一个榜单上强行保证。报表导出或审计查询应读取冻结的周期榜;实时榜只保证“某次请求时刻的一个窗口”,并把变化显式告诉调用方。

历史榜单的新模型:周期键本身就是快照

与其周日夜里复制一个不断写入的 live 键,我更喜欢把周期写进 key:lb:{game}:2026-W41。应用根据统一时区算出当前周期,整个周期只写这个键;周期结束后停止写入,它自然成为历史榜。花括号中的 {game} 还能让相关键在 Redis Cluster 中使用同一哈希标签。

# 当前周期持续累加,成员使用稳定用户 ID
redis-cli ZINCRBY 'lb:{game}:2026-W41' 30 'u:1004'

# 周期关闭后设置 90 天保留期;到期后 Redis 自动删除该历史键
redis-cli EXPIRE 'lb:{game}:2026-W41' 7776000

# 查询历史榜与实时榜使用同一种范围接口,只替换周期参数
redis-cli ZRANGE 'lb:{game}:2026-W41' 0 19 REV WITHSCORES
Redis 排行榜版本化翻页与周期历史键静态关系图
图2:分页与历史榜单的静态边界图。周期 Sorted Set 与版本键服务实时页面,冻结周期键服务历史查询,EXPIRE 管理保留期;调用方根据版本判断是否重新翻页。

周期切换要统一时区,并让旧周期进入只读状态。若业务允许迟到积分,应定义补写窗口以及是否同步修正已展示的历史名次;这属于产品规则,不能由 Redis 命令替你决定。

常见问题

同分时能直接使用 ZREVRANK 吗? 可以用于唯一位置榜,但不能用于“同分同名次”。后者需要基于严格更高分人数计算展示名次。

为什么不用分数游标翻页? 只保存 score 的游标无法区分一大组同分成员;持续写入时也无法保证稳定边界。需要严格一致时,应读取冻结周期键或专门生成快照。

历史榜设置 TTL 后继续 ZINCRBY 会丢失 TTL 吗? 修改现有数据结构内容通常不会清除超时,但关闭周期后仍应禁止业务写入,避免历史结果变化。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>