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