共计 1953 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点:高并发排行榜的技术挑战
在游戏或竞技平台中,技能排行系统需要处理两类核心场景:

- 高频写入:玩家每次完成对战都会触发分数更新,峰值期可能达到每秒数万次写操作
- 实时查询:前端需要毫秒级获取 TOP100 排名,且需支持分页查询任意区间的玩家排名
传统数据库方案(如 MySQL)面临三个致命问题:
- 更新操作需要先查询旧值,计算新值后再 UPDATE,产生大量磁盘 IO
- 排序操作需要全表扫描,时间复杂度 O(nlogn)
- 高并发时锁竞争导致性能急剧下降
技术选型:为什么选择 Redis ZSET
通过对比测试两种方案在 10 万级 QPS 下的表现:
| 指标 | Redis ZSET | MySQL+ 索引 |
|---|---|---|
| 更新延迟(avg) | 0.8ms | 45ms |
| TOP100 查询 | 1.2ms | 120ms |
| 内存占用 | 约 50MB/ 百万成员 | 约 600MB/ 百万记录 |
选择 ZSET 的核心优势:
- 底层采用跳表 + 哈希表,更新 / 查询时间复杂度都是 O(logN)
- 所有操作纯内存执行,无磁盘 IO 瓶颈
- 原生支持原子化的分数增减操作
核心实现方案
原子化分数更新
// 使用 ZINCRBY 实现原子更新
func UpdatePlayerScore(redisClient *redis.Client, playerID string, delta float64) error {
// 参数说明:// key: 排行榜名称(如 "openclaw:skill")// delta: 分数变化量(支持正负值)// playerID: 玩家唯一标识
cmd := redisClient.ZIncrBy("openclaw:skill", delta, playerID)
return cmd.Err()}
分段排名优化
当排行榜成员超过百万时,直接调用 ZRANGE 性能会下降。采用分段策略:
- 维护 TOP1000 的独立 ZSET,每 10 秒异步更新
- 查询时优先查 TOP1000 缓存,未命中再查全量 ZSET
- 使用 ZRANGEBYSCORE 实现分页查询
func GetRankRange(redisClient *redis.Client, start, stop int64) ([]redis.Z, error) {
// 优先查询缓存的热数据
if stop <= 1000 {return redisClient.ZRevRangeWithScores("openclaw:skill:top1000", start, stop).Result()}
// 大数据集分页查询
return redisClient.ZRevRangeWithScores("openclaw:skill", start, stop).Result()}
性能优化实践
基准测试数据
在 AWS c5.xlarge 节点(4vCPU 8GB 内存)的测试结果:
| 并发数 | 更新 QPS | 查询 QPS | P99 延迟 |
|---|---|---|---|
| 100 | 12,000 | 85,000 | 3ms |
| 1000 | 38,000 | 72,000 | 9ms |
| 5000 | 41,000 | 65,000 | 15ms |
内存优化技巧
- 使用
ZADD NX避免重复存储成员 - 启用 Redis 的 ziplist 编码(当元素 <128 且长度 <64 字节时自动生效)
- 定期执行
ZREMRANGEBYRANK清理低排名数据
避坑指南
同分排名处理
Redis 默认按字典序排列同分成员,可通过以下方案改进:
// 在原始分数后追加时间戳小数位
func GetPrecisionScore(baseScore float64) float64 {return baseScore + (1 - float64(time.Now().UnixNano())/1e19)
}
冷启动预热
系统启动时批量加载历史数据:
func WarmUpCache(redisClient *redis.Client, playerScores map[string]float64) {pipe := redisClient.Pipeline()
for id, score := range playerScores {pipe.ZAddNX("openclaw:skill", redis.Z{Score: score, Member: id})
}
pipe.Exec()}
集群一致性
在 Redis Cluster 环境中需注意:
- 相同 ZSET 的所有数据会 hash 到同一 slot
- 跨节点操作需要使用 HASHTAG 确保数据局部性
- 通过 WAIT 命令实现异步复制的最小一致性保证
总结与延伸
当前方案已能支撑百万级玩家的实时排行需求。如需扩展多维度排行(如地区榜、赛季榜):
- 使用多个 ZSET 存储不同维度的数据
- 通过 ZUNIONSTORE 实现聚合排行榜
- 为每个维度设置独立的过期时间
最终建议结合业务监控以下关键指标:
- ZSET 的内存增长速率
- 持久化操作的耗时
- 从库复制的延迟时间
通过本文方案的实施,我们成功将 OpenClaw 的排行榜查询延迟从 150ms 降低到 5ms 以内,同时节省了 80% 的服务器成本。希望这些实践经验对您有所启发。
正文完
