seckilllisted
Install: claude install-skill 1398281322-a11y/java-backend-guardrails
# 秒杀 / 抢购
## When to Invoke
限时、库存少、瞬时 QPS 远大于日常下单。文旅抢票、酒店今夜特价、券秒杀同样适用。
## When NOT
日常加购/下单 → `inventory-occupy` + `concurrency-sell`。不要把每个商品详情当秒杀。
## 风险(面试考点)
秒杀不是「Redis 锁 + 下单」。核心是 **把无效流量拦在数据库之外**,有效请求才扣库存。只靠 DB 行锁会打挂;只靠锁不带 `stock>=n` 会超卖。面试要能画出分层,而不是堆中间件名词。
## 分层(从外到内,默认全要,由外到内变严)
| 层 | 做什么 | 拦掉什么 |
|----|--------|----------|
| 1 页面 | 静态化 + CDN;开始前按钮置灰 | 刷新静态页 |
| 2 接入 | 网关按用户/IP/SKU 限流 | 刷子、超卖前的洪峰 |
| 3 资格 | 登录、活动时间、一人一单、验证码 | 不合格请求 |
| 4 预扣 | Redis Lua 原子扣 + 已买集合 | 无库存、重复抢 |
| 5 削峰 | 预扣成功才发 MQ 异步创单 | DB 写放大 |
| 6 落库 | `WHERE stock>=n` + 唯一键 | 缓存与 DB 最终一致 |
预扣失败直接售罄。禁止预扣失败还去锁 DB。
扣减细节读 `concurrency-sell`,热点拆段读 `hot-account`,关单读 `delay-job`。本文件不重复那些 SQL。本次最多再加 1–2 个,不要把上面三个全读进来。
## 默认方案
活动前把库存写入 Redis,按 SKU 分片(见 `hot-account`)。
```lua
-- KEYS[1] stock KEYS[2] boughtSet ARGV[1] userId ARGV[2] n
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -1 end
local s = tonumber(redis.call('GET', KEYS[1]) or '0')
if s < tonumber(ARGV[2]) then return 0 end
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('SADD', KEYS[2], ARGV[1])
return 1
```
成功 → RocketMQ(keys=userId+sku+activity)→ 消费者本地事务创单 + DB ���件扣。重复消息靠唯一键。
独立线程池、独立 Redis、必要时独立库。不要和商详、后台报表混部。
## 反例
错误:请求一进来 `RLock` + `select stock` + `update`。
正确:Lua 预扣,DB 只接预扣成功的量级。
错误:`DECR` 成负数还创单。
正确:Lua 里判断再减。
错误:MQ 消费者扣库存不带 `stock>=n`,重试把库存打成负数。
正确:条件更新 + 影响行数。
## 验证
- 库存 100,5 万 QPS,成交 ≤100,DB QPS 远小于入口。
- 同一用户并发只成一单。
- Redis 预扣了但创单失败,有回补或对账。
## 评审清单
- [ ] 有分层拦截,不是一把锁打天下
- [ ] Lua 预扣 + DB 条件更新两道
- [