跳转到内容

排行榜设计

💡

排行榜是几乎所有大型系统都会遇到的需求——微信步数、直播刷礼物、游戏排名、浏览量排行。本文从存储选型、架构设计、分布式扩展、冷热分离、大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和网络压力过重。

解决方案:定时预计算

  1. 设定阈值(如好友数 > 500 = 大V)
  2. 定时任务每5分钟为大V预计算好友排行榜,结果存入 Redis String bigv_rank:{userId}
  3. 大V查询时直接读取预计算好的排行榜缓存,不再实时扫描
  4. 大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问核心答案):

  1. 存储:Redis Zset, Key=step_rank:{date}:{slot}, Member=user:ID, Score=复合分数
  2. 好友排行榜:一个全量榜 + 好友Set + 批量ZSCORE + 内存排序
  3. 相同分数:score * 1e13 + (1e13 - timestamp),时间早的排前面
  4. 冷热分离:Redis(3天热) → ES(3月温) → Hive(长期冷)
  5. 大V用户:阈值判断 → 定时预计算排行榜缓存
  6. MQ削峰:Kafka缓冲 → 批量消费 → 积压时水平扩展+降级
  7. 千万用户:Redis Cluster,16384 hash槽水平分片
  8. Zset原理:dict(O(1)查分) + skiplist(O(log n)排序范围查)
  9. 实时vs性能:场景分级(微信步数异步 vs 直播实时WebSocket)
  10. 降级容错:Sentinel自动故障转移+本地缓存兜底+熔断
  11. 客户端同步:WebSocket增量推送(实时) / 轮询(低频)