如何使用 Redis 快速实现排行榜?
- 标签:后端、Redis
🏛 业务架构师
排行榜用 ZSet(有序集合):ZADD rank:{activity} score member 更新分数,ZREVRANGE 取 TopN、ZREVRANK 查自己名次,ZRANGEBYSCORE 取分数段,复杂度都在 O(logN)。十万级参赛者的榜单毫秒出结果。
业务落地我踩过的坑比 API 更值得讲:
- 同分排序规则要先和运营对齐:ZSet 同分默认按成员字典序。而业务上"同分先到先得"要把时间戳编进 score(如
score = 分数 × 10^10 + (到期时间戳的补数)),上线后再改规则等于榜单重算,很伤——规则要在需求评审时钉死; - 按时间维度拆 key:
rank:202610月榜独立,自然淘汰;全站总榜量大时只保留 TopN + 懒加载,key 的生命周期设计就是存储成本设计; - 只存 ID 不存详情:榜单存 userId,进详情页批量查用户服务,避免冗余和更新风暴;
- 更新走异步:点赞、消费等行为事件进 MQ,消费者聚合更新 ZSet,抗住写高峰(大促打赏榜瞬时 QPS 上万,同步写会打穿);
- 防刷与降级:刷分识别(频次突增)与"榜单服务挂了返回缓存的上一版快照"的降级预案。
场景延展:销量榜支撑营销页、积分榜驱动会员运营、直播打赏榜是收入核心——排行榜本质是"运营活动的实时仪表盘",技术上它是小需求,业务上它常常是流量入口,稳定性和规则准确性都要按核心链路对待。
🏗 系统架构师
排行榜用 Redis **ZSet(有序集合)**实现是标准答案,核心是它按 score 排序且提供排名查询:
核心操作:
- 加分/更新:
ZINCRBY rank:202610 1 user:1001(O(logN)); - 查排名:
ZREVRANK rank:202610 user:1001(第几名)、ZSCORE查分数; - Top N:
ZREVRANGE rank:202610 0 9 WITHSCORES; - 分页拉取:
ZREVRANGE带 offset/count; - 同分排序:score 设计成组合分(如
分数*1e10 + (MAX_TS - 时间戳)),先比分数再比早达到时间,一个字段解决多级排序。
架构权衡(生产必聊):
- 量级:单 key 百万成员没问题,亿级要按周期分片(月榜分 30 个日榜)或按分数段拆 key,聚合用 UNIONSTORE;
- 实时性:写操作先更新 Redis 再异步落库(消息队列削峰),Redis 是唯一读源,DB 只做持久化兜底;
- 冷热:历史榜单转压缩表归档,活跃榜常驻;
- 防刷:加分入口限流 + 幂等(同一行为只计一次,用 Set 或 DB 唯一约束去重)。
整个方案的本质是:把"排名"这个读多写多的计算,交给内存中 O(logN) 的有序结构,DB 退化为持久层。
💻 全栈开发专家
ZSET(有序集合)一把梭,member 存用户/对象 id,score 存分值:
bash
ZINCRBY rank:activity:1001 5 "user:42" # 加分
ZREVRANGE rank:activity:1001 0 9 WITHSCORES # Top 10(降序)
ZREVRANK rank:activity:1001 "user:42" # 某人的名次(0 起)
ZCARD rank:activity:1001 # 总参与人数java
// Spring 侧封装
boundZSetOps("rank:" + activityId).incrementScore(userId, score);
Set<String> top = boundZSetOps.reverseRange(0, 9);
Long rank = boundZSetOps.reverseRank(userId);两个必须处理的工程细节:
- 同分排序规则(产品常要求"同分先到者排前"):score 是 double,我把分数和时间揉进去:
score = 分数 * 1e10 + (MAX_TS - 时间戳),分数占高位、时间反序占低位,一条 score 同时表达两个排序维度。 - 大榜性能:ZSET 全量在一个 key,几十万 member 时 ZRANGE 也没问题,但实时分页拉全量会卡;百万级用户的活动我会分榜合并(按 userId 取模拆 100 个子榜,查询时各取 Top N 归并),或者只维护 Top 100 的精确榜 + 以下名次近似。
数据可靠性:排行榜是可重算数据,丢失后从 DB 日志重放即可,所以我会把 ZSET 操作记流水表,定时对账重建。