--- 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 和数据库之间通过对账保证最终一致。”