phase 6 part 1: eight cross-stack enterprise scenarios and scenarios index
This commit is contained in:
1 parent
691f738f5f
commit
5b1a7a430f
12 files changed
+1145
-12
No files matched your search
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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 和数据库之间通过对账保证最终一致。”
|
||||
Reference in new issue
Block a user