排行榜系统
概述
排行榜是游戏运营的"最强引擎":刺激竞争、引导消费、沉淀留存。排行榜的难点在于海量玩家实时排序、多周期分段(日/周/月/总)、同分并列的处理、以及榜单读多写少的缓存策略。Redis SortedSet(ZSet)天然支持按分数排序与范围查询,是排行榜的首选载体。本文设计完整方案:全服排行、分段排行、榜单缓存、同分排名。
一、SortedSet 基础
1.1 核心命令
ZADD key score member // 添加/更新成员分数
ZINCRBY key increment member // 分数自增(排行数据最常用)
ZREVRANGE key start stop // 按分数降序取区间(含分数)
ZRANK key member // 获取成员排名
ZREVRANK key member // 降序排名
ZSCORE key member // 获取成员分数
ZREMRANGEBYRANK key 0 n // 删除尾部 N 名(截断)1.2 适合排行榜的原因
ZSet 特性:
底层跳表 + 哈希表,增删改查 O(logN)
ZREVRANGE 支持按名次分页取
ZRANK 秒查某玩家名次
score 为 double,可编码复合信息二、排行榜模型
2.1 Key 设计
一个"榜"对应一个或多个 Key,通过榜单维度 + 周期组合:
key 设计:
rank:{board}:total // 总榜
rank:{board}:daily:{yyyyMMdd} // 日榜
rank:{board}:weekly:{yyyyWw} // 周榜
rank:{board}:monthly:{yyyyMM} // 月榜
示例:
rank:power:total // 战力总榜
rank:power:daily:20261212 // 战力日榜
rank:arena:weekly:202650 // 竞技场周榜日/周/月榜按时间分 Key,到期自然废弃,配合 Redis 过期时间自动清理,避免手工删除。
2.2 分数维度
排行榜分数通常不是单一数值,常见做法是组合分数编码:
方案一:score = 主分 + 次分 * 0.0000001
主分:战力/积分,次分:时间戳或等级
通过小数位承载并列判据,减少 ZSet 数量
方案二:排名计分(倒序法)
rankScore = MAX - 原始分,配合 ZADD 顺序
方案三:多 ZSet 联动
一个做主排序,一个记录原始分,查询时回填三、全服排行实现
3.1 数据写入
玩家产生排行数据时,异步更新各周期榜,避免同步写拖慢主流程:
更新流程:
1. 业务产生积分变动(如对战结算)
2. 写业务库(增量流水)
3. 异步任务执行:
ZINCRBY rank:power:total +delta
ZINCRBY rank:power:daily:{today} +delta
ZINCRBY rank:power:weekly:{week} +delta
ZINCRBY rank:power:monthly:{month} +delta
4. 记录该玩家"脏数据"状态异步更新要点:
批量合并:同一玩家短时间内多次变动,合并一次写
失败重试:MQ 消费失败重投,保证最终一致
幂等:按"流水ID"去重,防止重复加分3.2 榜单查询
查询逻辑:
ZREVRANGE key 0 99 // 前 100 名
ZREVRANK key playerId // 我的名次(+1 转排名)
ZSCORE key playerId // 我的分数
组合成榜单响应(昵称/头像从玩家服务批量补全)分页查询:
每页 100,ZREVRANGE key (page-1)*100 page*100
超过 1000 名不展示具体排名,只显示"1000+"
避免深分页性能问题3.3 我的排名
我的排名高频出现(详情页、结算页),用 ZREVRANK O(logN) 秒查。名次随他人变动浮动是正常的,可加刷新间隔缓存(30 秒内不重查)。
四、分段排行(日/周/月/总)
4.1 周期榜切换
日/周/月/总榜共用同一套写入逻辑,差异只在 Key 与结算时机:
日榜:
Key 按日期,次日 0 点换新 Key
昨日榜锁定(冻结),用于结算奖励
周榜:
Key 按周(ISO 周号),周一换新
月榜:
Key 按年月,月初换新
总榜:
常驻 Key,不清零,只做滚动清理(如 90 天无活跃移除)4.2 榜单结算
周期结束时需要冻结 + 发奖 + 重置:
结算流程(定时任务):
1. 冻结:将当前 Key 改名(rank:power:daily:1207 → rank:power:daily:1207:lock)
2. 快照:读取前 N 名,写入结算记录表
3. 发奖:通过邮件/直接发放发放奖励(幂等)
4. 清理:删除冻结 Key,释放内存结算必须幂等:结算任务崩溃重启后不重复发奖,用"结算批次号"去重。
4.3 多服/分服排行
单服榜:Key 天然隔离(加服号:rank:power:s1:total)
全服榜:跨服聚合,需上报中心服合并
分片 ZSet + 定期合并 → 合并榜(Overlay)
或统一走中心服排行榜服务(推荐,量小时简单)五、榜单缓存策略
5.1 读多写少的优化
排行榜是典型读多写少场景:榜单前 100 名被海量玩家反复查看,不必每次都查 ZSet。引入多级缓存:
缓存层级:
L1 本地缓存(Caffeine,5 秒过期):服务节点内
L2 Redis 缓存榜单 JSON(30 秒过期):跨节点共享
L3 Redis ZSet:最终数据源
查询路径:
先 L1 → 未命中查 L2 → 未命中查 L3 并回填 L1/L2缓存更新:
写入侧更新 ZSet 后,删除/失效缓存 Key
或依赖过期时间自然失效(榜单延迟 30 秒可接受)5.2 缓存一致性
排行榜允许短暂延迟(秒级),无需强一致。关键规则:
榜单查询允许 30 秒旧数据
我的名次允许 5 秒旧数据(体验优先)
结算时刻强制刷新,防止按旧榜发奖六、同分排名策略
6.1 并列问题
ZSet 中同分成员排序不保证稳定(按字典序排)。产品上常见两种诉求:并列同名次或用附加条件打破并列。
方案一:并列排名(展示层)
score 相同 → 相同名次(1、1、3)
查询时比较相邻分数,分数相等则名次相同
实现简单,无需改 ZSet
方案二:打破并列(数据层)
组合分数:score = 主分 + 时间因子
先达到该分数者排前(先到先得)
或后达者排前(后发优势,如冲榜活动)6.2 组合分数实现
先到先得实现:
score = 原始分 + (MAX_TS - 达成时间戳) * 1e-12
达成越早,小数部分越大,同主分时排前
注:double 精度有限,注意位数预算
后发优势实现:
score = 原始分 + 达成时间戳 * 1e-12
达成越晚,小数越大,后到排前精度预算:
double 有效数字约 15-16 位
主分 8 位 + 时间戳 10 位会溢出
应对:时间戳压缩(取分钟级差值)、
主分减位、或改用"主分整型 + 次分整型"双 ZSet更稳妥的替代:主分一个 ZSet,同分再查时间维度(ZRANGEBYSCORE 取同分区间,再按时间戳排序),只影响并列少数玩家,成本可控。
七、实现要点
核心接口:
updateScore(playerId, board, delta) // 异步加分
getBoard(board, page) // 榜单分页
getMyRank(board) // 我的名次
settleBoard(board, period) // 周期结算工程注意:
写入走异步队列,主线程不被阻塞
榜单缓存分级,命中率优先
大服榜单 Key 注意内存(百万成员 ≈ 数十 MB)
清理无用周期 Key,防 Redis 内存膨胀常见坑:
并发同分名次抖动 → 接受或组合分数
结算与写入并发 → 冻结先于读榜
缓存穿透(热榜被刷)→ 空值缓存 + 限流
跨服榜数据不一致 → 中心服统一维护与运营的结合:
冲榜活动(见限时活动章节)
排行奖励配置化(名次区间 → 奖励模板)
榜单公示与反作弊(异常分数审计,见行为检测章节)