面试知识库
进阶

排行榜设计#

一句话答案#

Redis ZSet 天然支持排行榜:ZADD 更新分数,ZREVRANGE 获取 TopN,ZREVRANK 查排名,O(logN) 操作。

核心要点

核心操作:

  • ZADD key score member:更新分数
  • ZREVRANGE key 0 9:Top 10
  • ZREVRANK key member:查排名

大规模优化: 分桶(按分数段) / 定时快照 / 近似排名

底层与精度陷阱深挖#

跳表为何 O(logN): 跳表是”多层有序链表”,在原始链表上随机抽取节点建上层索引。

  • 每个节点以概率 p(Redis 取 1/4)“晋升”到上一层,平均第 i 层节点数是第 i+1 层的 1/p,形成指数衰减的多级索引,期望层高 O(logN)。
  • 查找从最高层开始,每层尽可能向右跳,跳不动了下沉一层——本质是”二分”思想,每层排除掉一大段,期望比较次数 O(logN)。ZADD/ZREVRANK/ZRANGE 都基于这套结构。

Redis 为何选跳表而非红黑树: 二者查找都是 O(logN),但跳表赢在三点。

  • 范围查询友好: 排行榜核心是 ZRANGE(取第 X 到 Y 名)。跳表底层就是一条有序链表,定位到起点后顺着指针往后走即可连续取一段;红黑树取范围要做中序遍历、反复回溯父节点,实现复杂、缓存不友好。
  • 实现简单: 跳表插入/删除只需改几个指针 + 掷骰子定层高,没有红黑树的旋转、变色、平衡修复一堆边界情况,代码量小、不易出 bug。
  • 并发/内存可调: 层高由概率控制,调 p 可在内存与速度间权衡;无树旋转,锁粒度更易控制。

并列排名的精度陷阱(必点破): 常见做法是把”分数 + 时间戳”编码进一个 score 实现”同分先到者靠前”,如 score = 分数 × 10^13 + (MAX_TS − 时间戳)这是个有坑的方案:

  • Redis ZSet 的 score 是 IEEE 754 双精度浮点 double,尾数(mantissa)只有 52 位,能精确表示的最大整数是 2^53 ≈ 9.007 × 10^15。超过这个值,整数之间会出现”跳变”——相邻整数无法被区分,末位精度丢失。
  • 算一下:分数 × 10^13,只要分数 ≥ 901,乘积就 ≥ 9.01×10^15 > 2^53,已经溢出可精确表示范围。再叠加 13 位时间戳,低位的时间戳信息会被浮点直接抹掉——结果就是”同分用户的先后顺序失效,甚至不同分数被舍入成相同 score 导致排名错乱”。
  • 正确姿势: ① 严格控制编码后总位数不超过 2^53(如分数位数 + 时间戳位数 ≤ 15 位十进制),把时间戳压缩成相对偏移、降低精度;② 或干脆不在 score 里塞时间戳,同分时用 ZRANGEBYSCORE 取出全部同分 member,在应用层按业务字段二次排序;③ 真要高精度复合排序,改用「主 score 排序 + 辅助字段存 member 或单独结构」。一句话:double 的 52 位尾数是硬天花板,任何编码方案都要先算会不会越过 2^53。
面试回答(2分钟版)

排行榜设计的天选数据结构是Redis ZSet。它底层用跳表实现,核心操作都是O(logN):ZADD更新用户分数,ZREVRANGE获取TopN列表,ZREVRANK查询某个用户的排名。实时排行榜直接对ZSet操作即可,每次用户得分变化就ZADD更新。但数据量到千万级之后单个ZSet会有内存和性能压力,这时候有三种优化策略:第一是分桶,按分数段把一个大排行榜拆成多个ZSet,查询时先定位到对应分桶再查排名;第二是定时快照,对于不需要实时更新的排行榜比如日榜周榜,可以定时计算后缓存结果;第三是近似排名,亿级数据场景下用户不需要精确到每一名,可以用百分位或分段近似。并列排名的处理上,可以在score中编码时间戳,分数相同时先达到的排前面。

追问与易错

追问方向:

  • “实时和定时排行榜怎么选?”→ 实时排行(Redis ZSet 直接更新)适合竞争性强的场景如游戏积分榜;定时排行(每小时/每天批量计算后缓存)适合日榜/周榜等对实时性要求不高的场景,能大幅降低 Redis 压力
  • “数据量千万怎么办?”→ 单个 ZSet 存千万 member 内存开销大(约几 GB)且操作变慢;可以按分数段分桶存多个 ZSet,或者只维护 TopN(如 Top 10 万),其他用户排名通过分数估算近似位置
  • “并列排名怎么处理?”→ 在 score 中编码时间戳,如 score = 分数 * 10^13 + (MAX_TIMESTAMP - 实际时间戳),分数相同时先达到的排前面;或者用 ZRANGEBYSCORE 查出同分用户再按时间排序

易错点:

  • ❌ ZSET 能存无限数据——内存有限需清理
  • ❌ 忽略并发更新问题——ZADD 是原子的