排行榜设计
排行榜是几乎所有大型系统都会遇到的需求——微信步数、直播刷礼物、游戏排名、浏览量排行。本文从存储选型、架构设计、分布式扩展、冷热分离、大V用户、实时性权衡、降级容错等维度,给出一个可落地的高并发排行榜设计方案。
一、架构总览
核心设计:Redis Zset(热数据排序) + MQ(削峰填谷) + ES/Hive(冷数据持久化) + Redis Cluster(水平扩展)
二、存储结构:Redis Zset
2.1 为什么是Zset?
Zset = member(唯一成员) + score(排序分值)。底层在数据量较小时(<128个元素)使用ziplist;大数据量时使用dict + skiplist:dict实现O(1)查score,skiplist实现O(log n)范围查询和排名。
2.2 Zset常用命令
|
命令 |
说明 |
|---|---|
|
ZADD key score member |
添加/更新成员分数 |
|
ZSCORE key member |
查询某成员分数 |
|
ZRANGE key start stop WITHSCORES |
按分数升序获取范围 |
|
ZREVRANGE key start stop WITHSCORES |
按分数降序获取范围(Top N) |
|
ZRANK key member |
获取成员排名(升序,从0开始) |
|
ZREVRANK key member |
获取成员排名(降序) |
|
ZINCRBY key increment member |
增减成员分数 |
|
ZCARD key |
获取成员总数 |
2.3 Zset底层数据结构
跳表(Skip List):在原始有序链表上向上建立多层索引,实现快速定位。查找时从最高层开始,逐层下降,平均O(log n)。
- dict(哈希表):member → score 映射,O(1)查询分数
- skiplist(跳表):按score排序,O(log n)范围查询和排名
- 小数据优化:元素数<128 且 元素大小<64字节 → 使用ziplist(压缩列表,内存紧凑)
三、Key-Value设计
Q1:存储所有用户的微信步数,用什么结构?key和value分别是什么?
Key: step_rank:{date}:{slot},如 step_rank:20250415:0 Member: user:{userId} Score: 复合分数(步数 + 时间戳编码)
四、核心问题与解决方案
Q2:不同用户有不同好友,每个人要单独维护一个排行榜吗?
不需要!只需维护一个全量排行榜,每个用户有一个好友Set。查询时:拿好友Set → 批量ZSCORE获取每个好友的分数 → 内存中排序返回。
Q3:相同Score如何排序?
核心公式:score = actualScore × 10^13 + (10^13 - timestamp)
|
场景 |
方案 |
|---|---|
|
微信步数 |
分数相同则按时间排序(先达到的排前面) |
|
游戏段位 |
相同分数按时间戳+胜率+连胜场次等因素编码 |
|
自定义规则 |
把多维度规则编码进Score的低位部分 |
Q4:每天的数据都存Redis吗?如何做冷热分离?
Redis只保留近3天热数据,历史数据通过定时任务持久化到ES(可查询)或Hive(归档分析)。
|
数据层 |
存储 |
用途 |
|---|---|---|
|
热数据 |
Redis Cluster(3天) |
实时排行榜查询 |
|
温数据 |
Elasticsearch(3个月) |
近三个月历史排行榜查询 |
|
冷数据 |
Hive(长期) |
数据分析、报表、离线统计 |
Q5:大V用户有上万好友,怎么处理?
大V的好友ID分散在多个Redis hash槽中,批量ZSCORE查询耗时大,每次实时计算CPU和网络压力过重。
解决方案:定时预计算
- 设定阈值(如好友数 > 500 = 大V)
- 定时任务每5分钟为大V预计算好友排行榜,结果存入 Redis String bigv_rank:{userId}
- 大V查询时直接读取预计算好的排行榜缓存,不再实时扫描
- 大V的好友看到排行时,也走预计算缓存
Q6:为什么用到MQ?消息积压怎么解决?
大量步数写请求直接打Redis会造成压力过大。MQ实现削峰填谷 + 批量聚合。
消息积压解决方案
|
方案 |
说明 |
|---|---|
|
增加消费实例 |
Kafka增加partition + 消费者水平扩展 |
|
批量消费 |
Consumer一次拉取多条消息,batch ZADD写入Redis |
|
降级丢弃 |
高峰期丢弃非关键数据(如只保留每5分钟的采样),保证核心可用 |
|
限流 |
生产者端限流,超过阈值直接返回"稍后更新" |
Q7:千万级用户,单机Redis内存不够怎么办?
Redis Cluster将16384个hash槽分布到多台机器,CRC16(key) % 16384 确定slot位置。
- 水平扩展:增加节点即自动重新分配槽,突破单机内存上限
- 客户端直连:客户端根据key定位到正确节点,避免单点压力
- Key设计含slot信息:step_rank:{date}:{slot},确保同一天的排行榜数据尽可能在同一个节点
Q8:Redis Zset为什么可以当排行榜?
Zset = dict(Hash) + skiplist(跳表)。dict实现O(1)查分和去重(member唯一);skiplist实现O(log n)按分数排序和范围查询。两者结合天然适合排行榜场景。
Q9:如何平衡实时性和性能?
|
场景 |
实时性要求 |
方案 |
|---|---|---|
|
微信步数 |
低(几分钟延迟可接受) |
MQ异步 + 批量更新 + 定时刷新 |
|
直播刷礼物 |
极高(秒级甚至实时) |
直接写Redis + WebSocket推送排名变化 |
|
金融/赛事排名 |
极高 |
直写Redis + 事务保证一致性 + 降级预案 |
Q10:Redis集群主节点宕机怎么降级?
- 自动故障转移:Redis Cluster + Sentinel,从节点自动提升为主
- 本地缓存兜底:应用层本地缓存(Caffeine)存储上一份排行榜快照,Redis不可用时返回快照数据
- 静态榜单降级:返回前一天的排行榜数据(ES中),标注"数据可能延迟"
- 熔断机制:快速失败返回空榜单 + 友好提示,防止雪崩
Q11:实时榜单如何同步给客户端?
前端轮询(简单但浪费) → WebSocket推送(主播排名变化时主动推) → SSE(服务端推送事件)。
- 金融/赛事:WebSocket + 增量推送(只推排名变化的用户)
- 直播:WebSocket + 全量推送Top N(如Top 100)
- 微信步数:前端定时轮询(5分钟一次),MQ异步更新
五、核心代码
5.1 生产者:接收步数上报 → MQ
5.2 消费者:MQ → 批量更新Redis Zset
5.3 查询:好友排行榜
设计总结(11问核心答案):
- 存储:Redis Zset, Key=step_rank:{date}:{slot}, Member=user:ID, Score=复合分数
- 好友排行榜:一个全量榜 + 好友Set + 批量ZSCORE + 内存排序
- 相同分数:score * 1e13 + (1e13 - timestamp),时间早的排前面
- 冷热分离:Redis(3天热) → ES(3月温) → Hive(长期冷)
- 大V用户:阈值判断 → 定时预计算排行榜缓存
- MQ削峰:Kafka缓冲 → 批量消费 → 积压时水平扩展+降级
- 千万用户:Redis Cluster,16384 hash槽水平分片
- Zset原理:dict(O(1)查分) + skiplist(O(log n)排序范围查)
- 实时vs性能:场景分级(微信步数异步 vs 直播实时WebSocket)
- 降级容错:Sentinel自动故障转移+本地缓存兜底+熔断
- 客户端同步:WebSocket增量推送(实时) / 轮询(低频)