高并发下的排行榜系统:如何用Redis快速实现千万用户实时排名
高并发、千万用户、实时排名——听起来吓人拆开来看全是套路。本文从数据结构选型到工业级落地手把手带你打出一套 Redis 排行榜的组合拳附带完整代码可直接抄到生产环境。一、为什么是 Redis ZSet排行榜的核心需求就三件事分数实时更新、排名快速查询、TopN 批量拉取。Redis 的 Sorted Set有序集合是原生适配该场景的最优解底层采用压缩列表 跳表双结构实现时间复杂度表现优异。数据结构插入/更新排名查询TopN 查询适配排行榜ListO(N)O(N)O(1)❌ 完全不适用HashO(1)O(N)O(N)❌ 无法排序SetO(1)O(N)O(N)❌ 无排序能力ZSetO(log N)O(log N)O(log N M)✅ 原生适配千万数据量下ZREVRANK只需约 24 次比较log₂1000万 ≈ 23.3单次耗时约0.02ms。小数据量时 ZSet 用压缩列表省内存元素超过阈值自动切换为跳表兼顾内存与查询性能。内存估算千万成员每个 member score 约 50 字节总共约 500MB一台 Redis 轻松吃下。二、整体架构不能把流量直接打到 Redis单点 Redis 扛不住热 key 和突发流量必须分层设计。四层防线各司其职写入异步削峰积分变更先进 Kafka聚合后批量写 Redis避免瞬间打爆主库读写分离主节点只写从节点扛读可水平扩展从节点数量本地缓存扛热点Top100 榜单用 Caffeine 缓存 1 秒挡住 90% 读请求主动刷新热榜后台线程每秒拉取ZREVRANGE 0 99更新本地缓存用户拉取时零延迟关于这个问题的底层原理和更多实战细节我整理了一份《大厂面试手册》包含大厂高频面试题、源码解析和性能调优案例。关注公众号【Rain的Java大神之路】回复“Java”即可免费领取持续更新中。三、从百万到亿级渐进式优化路线3.1 基础版单 ZSet 搞定百万级百万用户规模内一个 ZSet 直接搞定// 更新用户积分 redisTemplate.opsForZSet().add(leaderboard:total, user: userId, score); // 查询用户排名降序返回 0 开始的名次1 转为业务排名 Long rank redisTemplate.opsForZSet().reverseRank(leaderboard:total, user: userId); long realRank rank null ? -1 : rank 1; // 查询 Top100 排行榜带分数 SetZSetOperations.TypedTupleString top100 redisTemplate.opsForZSet().reverseRangeWithScores(leaderboard:total, 0, 99);瓶颈在哪单 key 承载千万用户会形成大 keyRedis 单线程下写入 QPS 上限仅 3~5w无法承接高并发场景。3.2 进阶版水平分桶分片解决大 key 与写入瓶颈核心思路按用户 ID 哈希取模拆分为 N 个独立 ZSet例如分 128 桶leaderboard:0~leaderboard:127。写入hash(userId) % 128定位桶分散写入压力整体吞吐量线性提升个人排名先查当前桶内排名累加其余所有桶中分数高于当前用户的人数TopN 查询每个桶取前 N 名应用层小顶堆归并排序得到全局 TopN除非并发极高10w QPS 写一般千万级单 ZSet 完全够用先不做过度设计。3.3 终极版冷热分离兼顾性能与内存成本99% 的请求集中在 Top100 / Top1000尾部千万用户的排名查询占比极低全量放 Redis 浪费内存。热数据层Redis 仅存 Top 10000 用户支撑高频 TopN 和头部用户排名查询全量层全量用户分数落 MySQL / ClickHouse尾部用户排名走数据库 5 分钟缓存异步校准每分钟定时将全量数据的 TopN 刷新到 Redis保证最终一致性内存成本降低90% 以上同时保证头部热点数据毫秒级响应。四、四大核心技术难点逐个击破4.1 峰值写入削峰MQ 同用户聚合 Pipeline活动峰值签到、任务奖励秒刷会产生海量积分更新请求直接打 Redis 会触发限流甚至阻塞。解法用 Kafka 做削峰填谷消费侧做同用户变更聚合再用Redis Pipeline批量写入IO 次数减少 90%。/** * 技术亮点同用户变更聚合 Redis Pipeline 批量写入IO 次数减少 90% */ KafkaListener(topics leaderboard_score_update, groupId leaderboard-group) public void batchConsumeUpdate(ListScoreUpdateMsg msgList) { // 1. 同用户多笔变更聚合合并无效写入 MapLong, Double userDeltaMap new HashMap(); for (ScoreUpdateMsg msg : msgList) { userDeltaMap.merge(msg.getUserId(), msg.getScoreDelta(), Double::sum); } // 2. 按分桶分组同桶数据批量执行 MapInteger, MapString, Double bucketBatch new HashMap(); userDeltaMap.forEach((userId, delta) - { int bucketIdx getBucketIndex(userId); bucketBatch.computeIfAbsent(bucketIdx, k - new HashMap()) .put(String.valueOf(userId), delta); }); // 3. 每个桶用 Pipeline 批量执行减少网络 RTT 开销 bucketBatch.forEach((bucketIdx, scoreMap) - { String key getBucketKey(bucketIdx); redisTemplate.executePipelined((RedisCallbackObject) connection - { scoreMap.forEach((userId, delta) - connection.zIncrBy(key.getBytes(), delta, userId.getBytes()) ); return null; }); }); }收益牺牲百毫秒级实时性换取10 倍以上的写入吞吐量符合绝大多数业务近实时要求。4.2 同分排序位宽隔离加权零额外存储ZSet 默认同分按成员字典序排序不符合同分先达到者靠前的业务需求。解法通过分数加权设计将业务积分和时间戳编码到一个 double 值中最终分数 业务积分 × 10⁹ (最大时间戳 - 当前时间戳)积分高的用户永远靠前高位积分相同时达成时间越早时间差值越大最终分数越高10⁹ 的位宽保证时间戳不会影响高位业务积分的精度/** * 技术亮点位宽隔离设计零额外存储实现双维度排序 */ public class ScoreUtils { private static final double TIME_OFFSET 1_000_000_000.0; private static final long MAX_TIMESTAMP 4102444800L; // 2100-01-01 public static double buildFinalScore(double bizScore) { long timeDiff MAX_TIMESTAMP - System.currentTimeMillis() / 1000; return bizScore * TIME_OFFSET timeDiff; } public static double extractBizScore(double finalScore) { return Math.floor(finalScore / TIME_OFFSET); } }4.3 分桶后全局排名并行查询 小顶堆归并分桶后最大的挑战是全局排名查询——串行遍历所有桶耗时可达百毫秒级。解法多线程并行查询各桶计数 小顶堆多路归并计算 TopN查询耗时压缩至10ms 内。/** * 技术亮点多线程并行分桶查询 计数聚合全局排名查询耗时压缩 80% */ public Long getGlobalRank(Long userId) { int userBucket getBucketIndex(userId); String userKey getBucketKey(userBucket); Double userScore redisTemplate.opsForZSet().score(userKey, String.valueOf(userId)); if (userScore null) return -1L; Long bucketRank redisTemplate.opsForZSet().reverseRank(userKey, String.valueOf(userId)); long rankOffset bucketRank null ? 0 : bucketRank; // 并行查询其余桶中分数高于当前用户的总人数 ExecutorService executor Executors.newFixedThreadPool(8); ListFutureLong futures new ArrayList(); for (int i 0; i BUCKET_COUNT; i) { if (i userBucket) continue; int idx i; futures.add(executor.submit(() - { Long count redisTemplate.opsForZSet().count( getBucketKey(idx), userScore, Double.MAX_VALUE); return count null ? 0 : count; })); } long totalHigher 0; for (FutureLong future : futures) { try { totalHigher future.get(); } catch (Exception e) { log.error(分桶排名查询异常, e); } } executor.shutdown(); return totalHigher rankOffset 1; } /** * 技术亮点小顶堆多路归并求 TopN时间复杂度 O(M·logN)内存占用极低 */ public ListLeaderboardVO getGlobalTopN(int n) { ListSetZSetOperations.TypedTupleString bucketTopList new ArrayList(); for (int i 0; i BUCKET_COUNT; i) { SetZSetOperations.TypedTupleString topN redisTemplate.opsForZSet().reverseRangeWithScores(getBucketKey(i), 0, n - 1); if (topN ! null !topN.isEmpty()) bucketTopList.add(topN); } // 小顶堆实现多路归并排序 PriorityQueueZSetOperations.TypedTupleString minHeap new PriorityQueue(n, Comparator.comparingDouble(ZSetOperations.TypedTuple::getScore)); for (SetZSetOperations.TypedTupleString bucket : bucketTopList) { for (ZSetOperations.TypedTupleString tuple : bucket) { if (minHeap.size() n) { minHeap.offer(tuple); } else if (tuple.getScore() minHeap.peek().getScore()) { minHeap.poll(); minHeap.offer(tuple); } } } ListLeaderboardVO result new ArrayList(); while (!minHeap.isEmpty()) { ZSetOperations.TypedTupleString tuple minHeap.poll(); LeaderboardVO vo new LeaderboardVO(); vo.setUserId(Long.valueOf(tuple.getValue())); vo.setScore(ScoreUtils.extractBizScore(tuple.getScore())); result.add(0, vo); } for (int i 0; i result.size(); i) { result.get(i).setRank(i 1); } return result; }4.4 多级榜单原子更新Lua 脚本一招搞定日榜、周榜、月榜需要同时更新如果用多条命令存在部分失败的风险。用 Lua 脚本可以在 Redis 内原子执行多个操作-- 原子更新多个榜单 local uid KEYS[1] local delta tonumber(ARGV[1]) local today_key KEYS[2] local week_key KEYS[3] local month_key KEYS[4] redis.call(ZINCRBY, today_key, delta, uid) redis.call(ZINCRBY, week_key, delta, uid) redis.call(ZINCRBY, month_key, delta, uid) -- 设置过期时间避免内存无限膨胀 redis.call(EXPIRE, today_key, 86400 * 2) redis.call(EXPIRE, week_key, 86400 * 7 * 2)配合 Java 端调用RScript rScript redissonClient.getScript(); ListObject keys Arrays.asList(uid, rank:daily: today, rank:weekly: weekId, rank:monthly: monthId); rScript.eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.VALUE, keys, delta);五、本地缓存扛热点Caffeine 主动刷新Top100 榜单变化不频繁没必要每次请求都打到 Redis。用 Caffeine 做本地缓存不设过期随机失效而是主动定时刷新杜绝缓存击穿。private final CacheString, ListRankItem topNCache Caffeine.newBuilder() .maximumSize(10) .build(); Scheduled(fixedDelay 1000) // 每秒刷新 public void refreshTopN() { RScoredSortedSetString zset redissonClient .getScoredSortedSet(rank:daily: today); CollectionScoredEntryString entries zset.entryRangeReversed(0, 99); ListRankItem topList entries.stream() .map(e - new RankItem(e.getValue(), e.getScore())) .collect(Collectors.toList()); topNCache.put(daily_top100, topList); } // 查询接口直接返回本地缓存近乎零延迟 public ListRankItem getTop100() { return topNCache.get(daily_top100, k - Collections.emptyList()); }效果用户拉取 Top100 延迟 1ms90% 的读请求根本不会到达 Redis。六、性能参考数据方案支持用户规模写入 QPS排名查询耗时内存占用单 ZSet 基础版百万级3~5w1~3ms百 MB 级128 桶分片版千万级30~50w5~10ms1~2GB冷热分离版亿级50wTopN 1ms尾部 10~50ms百 MB 级场景方案延迟查自己排名读从库 / 本地缓存 1ms拉 Top100 榜单Caffeine 本地缓存 1s 刷新 1ms实时刷新 Top100主动拉 ZSet~2ms积分变更生效Kafka → Redis 异步写入50~200ms积分变动后延迟一两百毫秒刷新排名完全可接受——换来的是一整个量级的吞吐能力提升。七、技术难点全景对照表技术难点问题影响落地方案实际收益单 Key 大 key 风险千万级单 ZSet 体积达 GB 级阻塞 Redis 单线程用户 ID 哈希分桶分片拆分 N 个独立 ZSet彻底消除大 key 风险写入吞吐量线性提升峰值写入压垮集群活动峰值写入 QPS 10w同步直连易打满带宽MQ 削峰 同用户聚合 Pipeline 批量写入写入承载能力提升 10 倍以上分片后全局排名慢串行遍历所有桶耗时百毫秒级多线程并行查询 小顶堆多路归并排名查询耗时压缩至 10ms 内同分排序不符合业务ZSet 默认字典序无法满足先达成者靠前位宽隔离加权积分占高位、时间戳占低位零额外存储100% 匹配业务规则海量数据内存成本亿级全量存 Redis 成本极高冷热分离Redis 仅存热数据全量落库内存成本降低 90%多级榜单一致性日/周/月榜并发更新可能部分失败Lua 脚本原子更新多榜单严格原子性杜绝部分更新缓存击穿TopN 缓存过期瞬间大量请求穿透Caffeine 主动定时刷新不设随机过期延迟 1ms杜绝击穿主从切换丢数据主节点宕机从节点提升时可能丢失少量写入MQ 持久化 断点重放业务容忍少量回退极端场景数据安全兜底八、生产环境避坑指南绝对禁止ZRANGE key 0 -1全量遍历大 key会长时间阻塞 Redis 单线程引发雪崩分桶数量提前按 3 倍容量预估避免后续扩容的数据迁移成本活动类排行榜必须设置过期时间日榜 2 天、周榜 15 天、月榜 62 天下线后及时删除避免内存泄漏高频 TopN 查询加 1s 本地缓存抗读峰值大幅降低 Redis 压力ZREVRANK返回名次从 0 开始返回前端时必须 1 转为业务排名这个细节坑过无数人统一使用ZINCRBY增量更新避免ZADD覆盖导致并发扣分/加分乱序Kafka 按 uid 分区保证同一用户的消息顺序消费九、总结用 Redis ZSet 存分、Kafka 异步削峰写入、读写分离 Caffeine 本地缓存扛读、按时间维度拆分榜单避免热 key、哈希分桶解决大 key——这套组合拳就是千万级实时排行榜的标准解法。核心设计哲学就八个字分层治理渐进优化。百万级单 ZSet 足矣别过度设计千万级分桶分片 MQ 削峰 Pipeline 批量写入亿级冷热分离 Redis Cluster ClickHouse 混合方案如果业务发展需要多维度排行榜日榜、周榜、总榜按时间维度拆分独立 ZSet定时滚动归档如果突破亿级超大规模可基于 Redis Cluster 做跨节点分片或引入ClickHouse 物化视图 Redis 缓存的混合架构进一步扩展容量和并发能力。面试加分 Tip聊完基础方案后主动抛出同分排序怎么做分桶后全局排名怎么算异步写入一致性怎么保证这些进阶问题展示你对系统的深度思考比单纯堆方案更有说服力。

相关新闻

信奥的数学学习书籍(有代码和OJ)--2026.10

信奥的数学学习书籍(有代码和OJ)--2026.10

NOI数学 学习 相关书籍及视频等资料(不包括CTS内容) NOI数学 学习 相关书籍及视频等资料(不包括CTS内容)_noi教材-CSDN博客 1、C编程与信息学竞赛数学思维 王桂平等 著(2026.06) 2、C编程与信息学竞赛数学基…

2026/10/13 20:09:27 阅读更多 →
Agent Memory 详解:大模型智能体长期记忆架构

Agent Memory 详解:大模型智能体长期记忆架构

Agent Memory是让智能体从一次性工具升级为长期协作伙伴的核心基建。文章介绍了Agent Memory的核心定义、分层架构、检索机制和工程实现,结合LangChain标准化能力与OpenClaw本地持久化创新,提供了可直接落地的技术方案、完整代码示例与流程图。内容涵盖记…

2026/10/12 23:04:10 阅读更多 →
字节跳动128万年薪背后:小白也能入局的大模型应用开发黄金赛道

字节跳动128万年薪背后:小白也能入局的大模型应用开发黄金赛道

文章指出,大模型应用开发岗位需求激增,成为2026年春招热点,年薪普遍高于传统IT岗位。大模型应用开发主要分为RAG、Agent、Fine-tuning三条赛道,分别侧重于知识库搭建、智能体开发、模型微调。RAG需求量大,Agent增长最快…

2026/10/13 16:37:59 阅读更多 →

最新新闻

PS5串流实战指南:从局域网到外网的远程游戏配置与延迟优化

PS5串流实战指南:从局域网到外网的远程游戏配置与延迟优化

直接接PS5玩家一个很现实的问题:这台机器买回来之后,是不是基本就长在客厅电视柜上了?客厅被占、卧室想玩、出差想刷两把,每次都只能乖乖回到电视前。我捣鼓AnyPS5这套串流方案,就是为了把这台主机从HDMI线的长度里解放…

2026/10/13 20:09:06 阅读更多 →
若依vue3前端页面框架:路由联动、权限指令与缓存机制解析

若依vue3前端页面框架:路由联动、权限指令与缓存机制解析

简介:若依vue3前端页面框架是一套基于Vue3与Element Plus的开源后台管理前端解决方案,面向需要快速搭建企业级系统的开发团队及初中级前端工程师,适用于中后台管理系统、权限控制界面、数据看板等典型场景。压缩包共299个文件、约3MB&#xf…

2026/10/13 20:09:06 阅读更多 →
嵌入式视觉系统中MT9M001C到LCD的端到端时序设计

嵌入式视觉系统中MT9M001C到LCD的端到端时序设计

简介:本资源是面向FPGA初学者与嵌入式图像处理学习者的实战项目,聚焦于MT9M001 CMOS图像传感器的驱动与实时显示实现,适用于基于Altera/Intel FPGA平台的开发板验证与课程实验。项目完整呈现了从传感器数据采集、FPGA逻辑控制(含时…

2026/10/13 20:09:06 阅读更多 →
PS5全机型优化指南:从版本识别、M.2扩展到HDMI 2.1设置与故障排查

PS5全机型优化指南:从版本识别、M.2扩展到HDMI 2.1设置与故障排查

如果你手头有一台 PS5,或者正打算入手,不管是初版、轻薄版还是 Pro 版,这篇文章都是给你准备的。我写这个东西的起因很简单:群里隔三差五就有人问"我这个 PS5 为什么风扇声音这么大""到底要不要上 Pro""…

2026/10/13 20:09:06 阅读更多 →
STM32跑马灯实验详解:从时钟配置到GPIO驱动与工程下载

STM32跑马灯实验详解:从时钟配置到GPIO驱动与工程下载

简介:这份资源是面向嵌入式初学者与STM32入门者的跑马灯实验例程包,围绕GPIO输出控制这一核心知识点,帮助读者快速上手STM32微控制器的基本开发流程。资源以ALIENTEK MINISTM32开发板为硬件平台,配套完整工程代码与详细注释&#…

2026/10/13 20:09:06 阅读更多 →
Steinberg VST插件开发包入门:从零构建VST3音频插件

Steinberg VST插件开发包入门:从零构建VST3音频插件

简介:Steinberg VST插件开发包(VST3 SDK 3.5版)面向音频插件开发者、音乐软件工程师及数字音频工作站扩展爱好者,提供创建虚拟乐器与音频效果处理器所需的完整开发环境。包内共5764个文件,以html文档、png界面素材、h头…

2026/10/13 20:08:06 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →