如何用Redis实现基于Top N平均排名的玩家排行榜?
高效Redis实现方案及优化手段
一、核心思路:增量更新而非全量计算
不需要每次有榜单变更就遍历所有玩家重新计算,只针对排名发生变化的玩家进行局部更新,大幅降低计算成本。
1. 维护玩家的实时排名缓存
为每个玩家创建一个Redis Hash结构,键名为player:rank:{uid},字段对应10个榜单的标识(如board_1至board_10),值为该玩家在对应榜单的实时排名。
- 当某榜单(如
board_x)发生变动时,仅更新该榜单中排名变化的玩家缓存:- 通过
ZRANK或ZREVRANK获取玩家新排名(注意:Redis的排名从0开始,业务中若需从1开始,需加1转换) - 执行
HSET player:rank:{uid} board_x {new_rank}完成缓存更新
- 通过
2. 实时计算并更新平均排名
玩家的排名缓存更新后,立即计算其前5佳排名的平均值,并同步到总排名Sorted Set:
- 用Lua脚本封装全流程(减少网络往返开销),一次性完成「取缓存→筛选前5排名→计算平均→更新总榜单」:
-- 参数:玩家ID、榜单标识、新排名 local uid = ARGV[1] local board_key = ARGV[2] local new_rank = tonumber(ARGV[3]) -- 更新玩家排名缓存 local player_rank_hash = "player:rank:" .. uid redis.call("HSET", player_rank_hash, board_key, new_rank) -- 获取所有榜单排名并转数字数组 local raw_ranks = redis.call("HVALS", player_rank_hash) local num_ranks = {} for _, r in ipairs(raw_ranks) do table.insert(num_ranks, tonumber(r)) end -- 排序取最小的5个排名(排名越靠前数值越小) table.sort(num_ranks) local top5_ranks = {} for i = 1, math.min(5, #num_ranks) do table.insert(top5_ranks, num_ranks[i]) end -- 计算平均排名(转整数存储避免浮点精度问题) local total = 0 for _, r in ipairs(top5_ranks) do total = total + r end local avg_rank = total / #top5_ranks local avg_rank_int = math.floor(avg_rank * 100) -- 放大100倍转整数 -- 更新总排名Sorted Set redis.call("ZADD", "player:avg_rank", avg_rank_int, uid)
- 执行脚本时使用
EVAL命令,传入对应参数即可,Lua脚本在Redis端原子执行,保证数据一致性。
3. 总排名查询
直接通过ZRANGE player:avg_rank 0 -1 WITHSCORES(升序,对应平均排名越靠前越优先)或ZREVRANGE获取最终的玩家排序结果。
二、场景化优化
- 批量更新场景:若存在每日结算等批量更新大量玩家的场景,可将待更新玩家ID推入Redis List队列,后台Worker进程批量执行Lua脚本处理,避免瞬时压力过大。
- 内存优化:对长期无排名变动的玩家,定期清理其Hash缓存及总榜单中的记录(可通过Redis过期键或定时任务实现)。
- 非实时场景:若业务对实时性要求不高,可采用异步更新策略,将榜单变动事件放入消息队列,后台异步计算平均排名并更新总榜单。
三、关键注意事项
- 排名转换:明确业务中排名的起始值(是从0还是1开始),避免计算错误。
- 精度处理:用整数存储平均排名(如放大100倍),规避Redis Sorted Set的浮点精度丢失问题。
- 原子性:必须通过Lua脚本保证「更新缓存→计算平均→更新总榜单」的原子性,避免数据不一致。
内容的提问来源于stack exchange,提问作者pilaoda
相关产品推荐
相关产品推荐

