LeungTzeMeen
发布于 2026-05-30 / 7 阅读 / 0 评论 / 0 点赞

高并发架构演进实录:秒杀多级缓存设计与Lua原子扣减实践

在垂直电商的日常迭代中,秒杀或者大促抢购向来是最容易让后端开发失眠的场景。万级 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 事务消息 进行异步化结算:

  1. 促销中心 向 RocketMQ 发送一条 Half Message(半消息)。

  2. 收到 Broker 成功应答后,促销中心执行本地事务(记录秒杀流水)。

  3. 本地事务成功,促销中心向 MQ 发送 Commit 指令,消息正式对下游可见。

  4. 订单中心库存中心 异步消费这条消息,各自执行 2库16表 的落盘和物理锁库。

同时,我们在提单瞬间下发了一个 30分钟延迟队列。如果用户半小时内没有支付,延迟消息会自动触发逆向关单逻辑,反向回滚 Redis 库存并释放限购资格。

05. 突发险情:爆款单点热点 Key 怎么解?

在系统刚上线初期,我们曾通过 Grafana 监控看板捕捉到一个危险指标:某个垂直大 V 带货的一款超级爆款,在零点瞬间导致 Redis 集群的单分片节点网卡带宽几近打满,也就是经典的 Hot Key(热点Key)效应

为了防范局部过载,我们后来演进出了 Key 散列化隔离策略

在商品大促预热阶段,系统会自动把爆款 SKU 的库存拆分成多个子 Key 存储(例如 promo_sku_1001_1promo_sku_1001_2),并让它们均匀分布在不同的 Redis 分片节点上。用户抢购时,代码层根据 User_ID 的 Hash 值取模,动态路由到指定的子 Key 上执行 Lua 扣减。通过这种“化整为零”的打散战术,成功把单点压力摊平到了整个集群。

尾声总结

回看这一整套重构,高性能秒杀的本质其实就是用内存抗流量、用隔离防雪崩、用异步换响应。系统最终在保障核心 MySQL 绝对安全的前提下,实现了 QPS 3000+ 的稳健吞吐。在真实的架构演进中,没有毕其功于一役的银弹,根据业务监控指标不断进行微调与后置治理,才是最务实的落地思路。