news 2026/4/25 12:39:40

突破单节点 10 万 QPS 物理极限!揭秘秒杀系统底层的“库存分片”与降维打击

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
突破单节点 10 万 QPS 物理极限!揭秘秒杀系统底层的“库存分片”与降维打击


案发场景:
晚上 8 点,电商平台准点开启“茅台秒杀”。
你的代码写得很规范:使用 Lua 脚本包裹了GETDECR命令,保证了不超卖。

物理规则的无情碾压:
秒杀开启的第 0.01 秒,网关放进了 50 万个并发请求。
这些请求全部打向了 Redis 集群中存放stock:maotai的那台 Master 节点。
Redis 主线程疯狂地执行 Lua 脚本,但 CPU 毕竟是有物理极限的。它的处理速度上限也就是每秒 10 万次。
结果:剩下的 40 万个请求在 TCP 队列里死等,随后大面积触发Read timed out。更可怕的是,这台 Master 节点因为 CPU 100% 假死,引发了哨兵的误判,导致了集群主从切换,秒杀活动彻底流产。

极客的破局之道:
既然单线程的极限是 10 万,那我们就强行把它变成多线程!
怎么变?借用 Java 里ConcurrentHashMap的核心哲学:分段锁(分片)
我们把 1 个含有 10 万库存的超级 Hot Key,物理切割成 10 个含有 1 万库存的小 Key,分散到集群的 10 台不同机器上!


1. 核心原理解剖:化整为零的空间折叠魔法

面对单点写瓶颈,唯一的解法就是横向扩展(Scale Out)

第一步:库存切割(预热阶段)

在秒杀开始前,我们不写SET stock:maotai 100000
我们将这 10 万库存,均匀分成 10 份。
在 Redis 里生成 10 个不同的 Key:

  • stock:maotai:bucket:1= 10000
  • stock:maotai:bucket:2= 10000
  • stock:maotai:bucket:10= 10000

由于这 10 个 Key 的名字不同,按照 Redis Cluster 的CRC16算法,它们会被完美地散列到集群中的 10 台不同的 Master 节点上。

第二步:流量路由(秒杀进行时)

当 50 万个用户的请求打进网关时,Java 应用服务器如何决定去扣哪个桶的库存?
绝招:随机路由 / 用户 ID 路由。
我们可以在 Java 代码里,生成一个1 ~ 10的随机数,或者用User_ID % 10
把这 50 万流量,平均分发给这 10 个 Bucket!

奇迹发生:
原本 1 台机器扛 50 万并发(必死)。
现在变成了 10 台机器,每台机器只扛 5 万并发。Redis 单节点压力骤降 90%,如履平地般顺滑地完成了全部扣减操作!这就是并发写的降维打击


2. 致命的副作用:“库存碎片化”与“假性售罄”

架构没有银弹。库存分片虽然解决了并发性能问题,却引入了一个极其烧脑的业务逻辑漏洞:假性售罄

灾难推演:
秒杀到了尾声,库存快没了。

  • Bucket 1 还剩 0 个。
  • Bucket 2 还剩 5 个。
    此时,用户 A 发起抢购。他的User_ID刚好路由到了Bucket 1
    Java 应用去 Bucket 1 一查,发现库存为 0,于是直接向用户 A 返回:“商品已售罄!”
    结果用户 A 跑到微博上大骂:“我都看到页面上明明显示还有 5 件库存,你却提示我售罄?你们是在搞黑箱操作耍猴吗?!”

只要总库存 > 0,就绝对不能告诉用户售罄。这就是秒杀系统的绝对底线。


3. 终极防御:轮询借库与 Lua 合体技

为了解决假性售罄,我们必须实现**“借库(Fallback)”**机制。

当路由到的 Bucket 没有库存时,不能立刻失败,而是要去其他 Bucket 里“碰碰运气”。

高阶工作流:
  1. 随机定位到一个起始桶,比如Bucket 3
  2. 尝试用 Lua 脚本在Bucket 3扣减。
  3. 如果Bucket 3库存不足,立刻尝试Bucket 4,然后是Bucket 5直到循环遍历完所有的 10 个桶。
  4. 如果 10 个桶全都扣减失败,才能真正宣告“售罄”。
灾难再临(网络风暴):

如果你在 Java 里面用个for循环,挨个去调 Redis 的 Lua 脚本。
遇到极端情况(前 9 个桶都空了),你扣减一次库存,竟然要在客户端和 Redis 之间发起9 次网络 RTT 通信!这会让应用服务器的网络连接池瞬间爆满。

大厂的破局解法:合并路由与单节点合并

  1. 不要分太多桶:桶的数量通常等于 Redis Master 节点的数量(比如 3 个或 5 个)。
  2. 在 Redis 服务端做合并:对于要求极其严苛的场景,可以开发 Redis C 语言底层模块(Module),在服务端直接进行跨 Slot 的库存协调。但绝大多数情况下,Java 端的少量for循环重试已经足够,因为“售罄”通常发生在最后几秒,此时整体并发流量已经大降。

4. 代码落地:Spring Boot 库存分片扣减实战

下面是一套支持“失败轮询”的库存分片扣减逻辑。

importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importjava.util.Collections;importjava.util.concurrent.ThreadLocalRandom;@ServicepublicclassShardedInventoryService{privatefinalStringRedisTemplateredisTemplate;privatefinalDefaultRedisScript<Long>deductScript;privatestaticfinalintBUCKET_COUNT=10;// 总桶数publicShardedInventoryService(StringRedisTemplateredisTemplate){this.redisTemplate=redisTemplate;this.deductScript=newDefaultRedisScript<>("if tonumber(redis.call('get', KEYS[1]) or '0') > 0 then "+" return redis.call('decr', KEYS[1]) "+"else "+" return -1 "+"end",Long.class);}/** * 核心逻辑:分片扣减库存,支持失败借库 */publicbooleandeductInventory(StringgoodsId){// 1. 随机生成一个起始的 Bucket 编号intstartBucket=ThreadLocalRandom.current().nextInt(BUCKET_COUNT)+1;// 2. 遍历所有 Bucket,尝试扣减for(inti=0;i<BUCKET_COUNT;i++){// 计算当前尝试的桶编号 (环形遍历: 3, 4, 5...10, 1, 2)intcurrentBucket=((startBucket+i-1)%BUCKET_COUNT)+1;StringbucketKey="stock:"+goodsId+":bucket:"+currentBucket;// 3. 执行 Lua 脚本扣减LongremainStock=redisTemplate.execute(deductScript,Collections.singletonList(bucketKey));if(remainStock!=null&&remainStock>=0){// 扣减成功,立刻返回!System.out.println("✅ 扣减成功!命中的桶是: "+currentBucket);returntrue;}// 返回 -1 说明当前桶空了,继续下一次循环尝试其他桶}// 3. 所有桶都遍历完了,依然失败,才是真正的售罄!System.err.println("❌ 绝对售罄!所有桶均无库存。");returnfalse;}}

5. 架构师的延伸:与 JavaLongAdder的跨时空共鸣

如果你熟悉 Java 并发包(JUC),你会发现 Redis 的这种“库存分片”思想,和 JDK 1.8 中引入的极其牛逼的并发工具类LongAdder的底层源码思想如出一辙

在以前的AtomicLong中,多个线程在高并发下使用 CAS(比较并交换)去修改同一个数字,会导致疯狂的死循环重试,榨干 CPU(这就等同于全部打向 Redis 的一个 Key)。

LongAdder底层维护了一个叫Cell[]的数组。当并发量高时,它把不同的线程通过 Hash 散列到不同的Cell里去独立累加(等同于 Redis 的 Bucket)。最后求总数时,再把所有Cell的值加起来。

优秀的架构思想,无论是在单机 JVM 内存里,还是在跨机房的分布式集群中,永远都是相通的。时间不够,空间来凑;单点不行,分而治之。


总结

当流量小的时候,系统拼的是代码规范和算法复杂度;
当流量大到一定程度,系统拼的就成了物理学常识。

Redis 的“单线程高性能”神话,在百万级并发写面前依然会被打破。
通过“库存分片”,我们将系统对 CPU 单核的时钟周期压榨,转化为了对多台机器网卡和多核 CPU 的整体调度。配合精妙的 Failover 轮询机制,我们在性能极限与数据严谨性之间,找到了最美的平衡。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 12:38:36

告别串口调试:用Python和FT232H玩转GPIO,5分钟实现硬件信号控制

告别串口调试&#xff1a;用Python和FT232H玩转GPIO&#xff0c;5分钟实现硬件信号控制 在硬件开发的世界里&#xff0c;调试工具的选择往往决定了效率的高低。传统单片机开发需要专门的调试器和复杂的IDE环境&#xff0c;而串口调试虽然简单但功能有限。有没有一种方法&#…

作者头像 李华
网站建设 2026/4/25 12:38:36

投资顾问转数据分析适合哪些岗位?客户分析、投研支持还是经营分析

投资顾问转数据分析的岗位适配性分析投资顾问转型数据分析具有天然优势&#xff0c;其金融行业经验、客户需求理解能力与数据分析技能结合&#xff0c;可适配以下三类岗位&#xff1a;客户分析岗位适配性技能要求CDA证书加分项高用户画像构建、行为数据分析、ROI评估CDA课程涵盖…

作者头像 李华
网站建设 2026/4/25 12:37:52

Python数据科学全家桶:从零部署pandas、numpy、matplotlib与statsmodels

1. 为什么需要Python数据科学全家桶&#xff1f; 刚接触Python数据科学的新手常会遇到这样的困惑&#xff1a;明明跟着教程安装了pandas&#xff0c;运行时却提示numpy缺失&#xff1b;好不容易装好matplotlib&#xff0c;又发现statsmodels无法导入。这些库之间存在复杂的依赖…

作者头像 李华
网站建设 2026/4/25 12:36:51

3D全景视频转2D观看:无需VR设备的终极解决方案

3D全景视频转2D观看&#xff1a;无需VR设备的终极解决方案 【免费下载链接】VR-reversal VR-Reversal - Player for conversion of 3D video to 2D with optional saving of head tracking data and rendering out of 2D copies. 项目地址: https://gitcode.com/gh_mirrors/v…

作者头像 李华
网站建设 2026/4/25 12:36:44

Policy Learning实战避坑:REINFORCE和Actor-Critic到底该怎么选?

Policy Learning实战指南&#xff1a;REINFORCE与Actor-Critic的工程化选择 在强化学习领域&#xff0c;策略优化&#xff08;Policy Learning&#xff09;一直是解决复杂决策问题的核心方法。不同于基于价值的传统方法&#xff0c;策略学习直接对策略进行建模和优化&#xff0…

作者头像 李华