在垂直电商的日常迭代中,秒杀或者大促抢购向来是最容易让后端开发失眠的场景。万级 QPS 在零点瞬间涌入,如果系统底座不够稳,磁盘 I/O 剧增、数据库连接池瞬间占满、服务雪崩往往就在一瞬间。
前几年在重构公司的核心促销中心时,团队经历过一次深刻的架构演进。今天把当时为了降伏洪峰而设计的防御链路,以及中间踩过的热点坑完全公开,聊聊如何用最务实的方案做到整体异步与层层拦截。
01. 流量第一站:在网关层“泼冷水”
请求刚进网关 Spring Cloud Gateway,就必须进行第一轮清洗,绝对不能让无节制的流量去冲击核心微服务。
我们当时引入了 Sentinel,配置了标准的 漏桶算法 (Leaky Bucket) 进行热点 URL 限流。这个算法的底层逻辑非常简单:它像一个漏斗,不管上游进水(流量)多猛,流出的速度永远是恒定的。超出的过载流量或恶意刷单请求,在网关层就会直接触发熔断降级。在这个阶段,我们成功把大约 40% 的无效和过载流量直接挡在了系统之外。
02. 应用内存防线:用 Caffeine 给 Redis 减负
流量进到促销中心(Promo Service)集群后,真正的硬仗才开始。在早期版本中,我们图省事,每次抢购都直接去查分布式 Redis。但大促时的爆款商品是极端热点 Key,全网流量瞬间集中在单个 Redis 分片节点上,那台机器的 CPU 瞬间就会被顶满。
为了解决这个问题,我们在微服务节点内部引入了本地缓存 Caffeine。
状态秒级同步: 节点每隔 5 秒异步去同步一次 Redis 的活动状态与当前库存水位。
内存级拦截: 如果本地 Caffeine 发现活动没开始,或者库存已经为 0,请求在 JVM 内存层直接被拒绝,根本没有机会去消耗网络 I/O。这就为下游的 Redis 筑起了一道坚固的物理隔离带。
03. 核心击杀点:Redis + Lua 脚本原子扣减
通过了内存校验的有效流量,才允许进入真正的库存扣减。在面对瞬时并发时,我们坚决抛弃了重量级的分布式锁,而是把核心的业务逻辑打包成了一段 Lua 脚本 直接扔给 Redis 执行:
lua
local skuKey = KEYS[1] -- 商品库存 Key
local userKey = KEYS[2] -- 用户限购 Key
local userId = ARGV[1] -- 用户 ID
local requestCount = tonumber(ARGV[2])
-- 校验用户是否重复购买
local hasPurchased = redis.call('sismember', userKey, userId)
if hasPurchased == 1 then
return -1
end
-- 校验并扣减库存
local currentStock = tonumber(redis.call('get', skuKey) or "0")
if currentStock >= requestCount then
redis.call('decrby', skuKey, requestCount)
redis.call('sadd', userKey, userId)
return 1
end
return 0
请谨慎使用此类代码。
因为 Redis 执行 Lua 脚本是单线程串行的,所以在 Redis 内存中,“校验、扣减、记录限购”这三个动作被拧成了一个不可分割的原子操作。这种做法在彻底杜绝“超卖”的同时,由于省去了分布式锁的反复获取与释放过程,执行效率极其强悍。
04. 后续流程:RocketMQ 事务消息全异步结算
只要 Lua 脚本返回 1,就代表这个用户抢到了购买凭证。促销中心会立刻给前端下发下单令牌(Token),至于后续的生成订单、扣减优惠券、锁物理库存等重度交易链路,全部交由 RocketMQ 事务消息 进行异步化结算:
促销中心 向 RocketMQ 发送一条
Half Message(半消息)。收到 Broker 成功应答后,促销中心执行本地事务(记录秒杀流水)。
本地事务成功,促销中心向 MQ 发送
Commit指令,消息正式对下游可见。订单中心 和 库存中心 异步消费这条消息,各自执行 2库16表 的落盘和物理锁库。
同时,我们在提单瞬间下发了一个 30分钟延迟队列。如果用户半小时内没有支付,延迟消息会自动触发逆向关单逻辑,反向回滚 Redis 库存并释放限购资格。
05. 突发险情:爆款单点热点 Key 怎么解?
在系统刚上线初期,我们曾通过 Grafana 监控看板捕捉到一个危险指标:某个垂直大 V 带货的一款超级爆款,在零点瞬间导致 Redis 集群的单分片节点网卡带宽几近打满,也就是经典的 Hot Key(热点Key)效应。
为了防范局部过载,我们后来演进出了 Key 散列化隔离策略:
在商品大促预热阶段,系统会自动把爆款 SKU 的库存拆分成多个子 Key 存储(例如
promo_sku_1001_1,promo_sku_1001_2),并让它们均匀分布在不同的 Redis 分片节点上。用户抢购时,代码层根据User_ID的 Hash 值取模,动态路由到指定的子 Key 上执行 Lua 扣减。通过这种“化整为零”的打散战术,成功把单点压力摊平到了整个集群。
尾声总结
回看这一整套重构,高性能秒杀的本质其实就是用内存抗流量、用隔离防雪崩、用异步换响应。系统最终在保障核心 MySQL 绝对安全的前提下,实现了 QPS 3000+ 的稳健吞吐。在真实的架构演进中,没有毕其功于一役的银弹,根据业务监控指标不断进行微调与后置治理,才是最务实的落地思路。