设计秒杀系统:存储与缓存方案怎么落地?
结论先行:秒杀的矛盾是海量请求打极窄的库存资源,方案核心是分层挡流 + 原子扣减 + 异步落库:静态流量打到 CDN,资格校验与限流放网关,库存预扣放 Redis(Lua 原子),最终扣减回数据库事务,订单与通知走 MQ 削峰异步化。防超卖靠“同一条库存记录的原子写”,而不是靠应用锁堆叠。
一、流量分层
| 层 | 承担什么 | 关键动作 |
|---|---|---|
| CDN/页面缓存 | 静态页与活动页 | 动静分离、秒杀页不做动态渲染 |
| 网关/入口 | 防刷、限流、风控 | 验证码、令牌桶、黑名单 |
| 应用层 | 资格校验、Redis 预扣 | 只放行拿到资格的请求 |
| MQ | 削峰、解耦 | 下单请求排队异步处理 |
| DB | 最终扣减与落单 | 事务 + 行锁,防超卖收口 |
二、库存扣减:防超卖的关键
-- 数据库原子扣减(同一行串行化,天然防超卖)
UPDATE stock SET remain = remain - 1 WHERE sku_id = ? AND remain > 0;
-- 影响行数 = 0 说明库存不足
-- Redis 预扣:Lua 保证“检查 + 扣减”原子
local r = tonumber(redis.call('GET', KEYS[1]))
if r and r > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 抢到资格
end
return 0 -- 已售罄
- 预热:活动开始前把库存写入 Redis,DB 库存是最终对账的底账。
- 兜底对账:Redis 扣减流水与 DB 库存定期对账,差异靠补偿单修正。
三、缓存与存储的配合
- 商品详情/价格缓存:Redis 或本地缓存,秒杀期命中率极高;防穿透加布隆过滤器/空值缓存。
- 用户已购状态:Redis SETNX 防重复下单,幂等键落库。
- 下单异步化:抢到资格即返回排队中,订单创建、扣库存、发券全部走 MQ 消费。
四、设计红线
- 超卖红线:任何路径都不能绕过 remain > 0 的原子更新。
- 少卖红线:MQ 消费失败要有重试与死信,DB 扣减失败要回补 Redis 库存。
- 体验红线:请求打满前先被限流/排队挡住,而不是把数据库打挂后再 502。
常见追问与记忆点
- 追问:能不能不加 Redis 直接全走 DB?能,扛得住就行;秒杀本质是省掉昂贵的 DB 写与行锁争用。
- 追问:减库存放在下单前还是支付后?下单前预扣防“下单成功却无货”,支付成功才算终态,超时未支付回补库存。
- 记忆点:CDN 挡静态、网关挡流量、Redis 原子预扣、MQ 削峰、DB 行锁收口;防超卖靠原子更新,对账保最终一致。