Files

85 lines
4.2 KiB
Markdown

---
id: scenario.flash-sale
title: 秒杀 / 高并发抢购:流量削峰、Redis 原子扣库存、异步下单、防超卖和防刷
type: scenario
domain: cross
stack: [redis, mq, postgresql, gin, nginx, vue]
tags: [秒杀, 高并发, 库存, Lua, 削峰, 异步下单, 幂等, 限流, 防刷]
level: advanced
prerequisites: [backend.redis.index, backend.mq.index]
est_hours: 3
related: [backend.redis.scenario.stock-reservation, backend.mq.scenario.delayed-order-cancel, backend.redis.lock-ratelimit]
week: [3, 14, 16]
---
# 秒杀 / 高并发抢购:流量削峰、Redis 原子扣库存、异步下单、防超卖和防刷
> 经典的后端面试场景题。你的主线是 AI 应用,但“高并发怎么设计”几乎每个后端 / 全栈面试都会问。
## 业务背景与问题
电商平台做限量 1000 件的秒杀,预计瞬间几十万请求:
1. 上次活动数据库被打垮,整个商城不可用;
2. 出现超卖(卖出 1003 件);
3. 同一用户用脚本抢到多件;
4. 用户付款超时后库存没有释放。
## 约束
- 活动时间固定,流量可预期。
- 不能影响商城的其他功能。
- 库存数据最终以数据库为准。
## 方案设计
1. **前端和网关削峰**:静态页面走 CDN;按钮到点才可点击、点击后置灰;验证码或答题打散请求;Nginx / 网关按 IP 和用户限流。
2. **独立部署**:秒杀服务独立,和主站隔离,挂了不影响主站。
3. **预热**:活动前把库存加载到 Redis。
4. **原子扣减**:Redis Lua 脚本一次完成“检查是否已购买 → 检查库存 → 扣减 → 记录用户”,避免超卖和重复购买。
5. **异步下单**:扣减成功后发消息,下单服务按数据库能承受的速率消费;数据库层 `UPDATE stock SET n = n - 1 WHERE id = ? AND n > 0` 再兜底一次,订单表用户 + 活动唯一约束保证幂等。
6. **结果查询**:前端轮询“排队中 / 成功 / 失败”。
7. **超时释放**:订单创建时投递延迟消息,到期未支付则取消订单、库存加回 Redis 和数据库。
8. **防刷**:登录态 + 设备指纹 + 风控规则;隐藏真实下单地址(到点才下发带签名的令牌)。
## 关键代码
Redis Lua 原子扣减(可以直接在 Redis 中运行):
```lua
-- KEYS[1]=库存 key KEYS[2]=已购用户集合 ARGV[1]=用户 id
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return -1 -- 已经抢过
end
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then
return 0 -- 售罄
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1 -- 成功,随后发消息异步下单
```
预占库存与超时释放的完整讨论见 [Redis 库存预占](../topics/backend/redis/scenarios/stock-reservation.md)、[延迟消息取消订单](../topics/backend/mq/scenarios/delayed-order-cancel.md)。
## 取舍
| 决策 | 选择 | 原因 |
| --- | --- | --- |
| 扣库存位置 | Redis 原子扣减 + 数据库兜底 | 数据库扛不住瞬时流量 |
| 下单 | 异步(消息队列) | 削峰,按数据库能力消费 |
| 一致性 | 最终一致 + 对账 | Redis 和数据库之间可能短暂不一致 |
| 用户体验 | 排队中 + 轮询结果 | 换取系统稳定 |
## 易错点
- 先查库存再扣减(两步操作),并发下超卖。
- Redis 扣减成功但消息发送失败,库存“丢失”:需要补偿(本地消息表 / 对账)。
- 数据库下单没有唯一约束,消息重复消费产生重复订单。
- 超时取消时没有检查订单是否已支付。
- 秒杀服务和主站共用数据库连接池。
## 面试怎么讲
> “秒杀我分层削峰:CDN 静态页、按钮控制和验证码打散、网关限流;秒杀服务独立部署。库存提前加载到 Redis,用 Lua 脚本原子完成‘是否已购、库存检查、扣减、记录用户’,杜绝超卖和重复购买。扣减成功后发消息异步下单,数据库用条件更新再兜底,订单表唯一约束保证幂等;未支付由延迟消息超时释放库存。Redis 和数据库之间通过对账保证最终一致。”