秒杀抢购高并发
秒杀抢购是高并发场景中最棘手的挑战,核心矛盾在于:极短时间内,海量请求争抢极少量库存。
处理秒杀的核心哲学是:把流量尽量拦截在上层(缓存/应用层),把压力尽量挡在数据库外面,并用异步削峰让流量“排队”。
下面是一套经过大厂验证的秒杀系统经典架构全链路方案(从用户点击到下单成功):
第一阶段:流量“过滤”与“拦截”(防刷与减负)
在请求到达业务逻辑之前,先做好“减法”,把绝大多数无效流量挡在外面。
- 前端/客户端“按钮置灰”:点击抢购后立即置灰按钮,限制 3-5 秒内不可重复点击,防止用户手抖产生重复请求。
- 静态资源预热与CDN:秒杀页面完全静态化,直接放到CDN,用户打开页面不经过后端服务器。
- IP/用户限流:在网关层(如Nginx或Sentinel)针对单个IP或用户ID限流(例如每秒只允许通过5个请求),阻止脚本小子利用机器疯狂刷接口。
- 答题验证码/滑块:在点击抢购前强制输入验证码。目的不是防机器人,而是“人为拉长请求间隔”,打乱自动化脚本的节奏,能瞬间过滤掉大量机器流量。
第二阶段:库存预扣(Redis 原子操作)—— 核心瓶颈点
这是秒杀最关键的一步。绝对不能让请求直接打向数据库,所有库存操作必须在内存(Redis)中完成。
- 库存预热:秒杀开始前,将商品库存数量(例如
stock:1001)和商品详情数据提前加载到 Redis 中。 - Lua 脚本原子扣减:使用 Redis 的原子性 Lua 脚本,判断库存 > 0 则执行
DECR(减1)。这一步在单线程的Redis中天然无并发冲突,性能极高(单机可达数万QPS)。- 返回成功:代表“抢到名额”,进入下一阶段。
- 返回失败:直接返回“已售罄”,绝不操作数据库。
- 库存“热Key”打散:如果百万人都抢同一个商品(热Key),Redis单节点压力也会巨大。可以给库存拆分为10个分片(如
stock_01~stock_10),用户请求随机路由到某个分片扣减,分摊压力。
第三阶段:异步削峰与排队(消息队列 MQ)
Redis扣减成功只是“占坑”成功,真正的订单数据落地需要靠消息队列,防止数据库被写垮。
- 发送MQ消息:Redis扣减成功后,立即将“下单请求”发送到消息队列(如RocketMQ/Kafka)中,并立即返回给用户“排队中,请稍候”。
- 后端消费者慢慢拉取:后端服务作为消费者,以数据库能承受的速率(例如每秒 500 条)从MQ中拉取消息,依次创建订单、扣减数据库实际库存。
- 最终一致性:这一步允许短暂的数据不一致。只要MQ消息不丢,最终订单一定会落地。
第四阶段:数据库落地与防超卖(最终防线)
虽然数据库不是主要抗压点,但它是最终兜底,绝对不能出现“超卖”(库存扣成负数)。
- 数据库乐观锁:更新库存时必须带版本号或库存条件判断。
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;
- 唯一索引防重:订单表针对
用户ID + 商品ID建立唯一联合索引。即使消息重复消费或用户重复抢购,插入订单时数据库会报错,保证一个用户只能买一件。
第五阶段:结果同步与用户感知
异步处理完后,需要把结果告诉用户。
- 轮询/WebSocket推送:前端页面设置定时轮询(或WebSocket长连接),不断查询订单状态。当订单创建成功后,通知用户“抢购成功,去支付”;如果MQ排队超时,则通知“抢购失败,已补货”。
💡 务必谨记的“三把锁”与“一个原则”
- 先删缓存后更新DB?还是先更新DB后删缓存? 秒杀场景下,采用先扣Redis缓存,后发MQ异步落库。如果DB落库失败(极少发生),需要发送反向消息回滚Redis库存。
- 关于“支付”:很多人忽略这一点。秒杀抢到名额≠交易成功。必须设置支付超时(如15分钟),超时未支付要回滚Redis库存,把机会释放给后面排队的人。
- 兜底原则(熔断降级):如果Redis挂了或MQ积压严重,立刻开启降级:关闭秒杀入口,页面显示“活动过于火爆,稍后再试”,宁可丢请求,绝不拖垮整个网站。
🔥 一张图总结流程(极简版)
用户请求 → 网关限流/验证码 → Redis Lua原子扣库存(失败则售罄) → 成功则发送MQ消息 → 立即返回“排队中” → 后端拉取MQ → 数据库乐观锁落单 → 更新最终结果给前端。
如果你正在代码实现阶段,我可以为你提供 Redis+Lua 扣库存 和 乐观锁 SQL 的具体代码示例,需要的话告诉我你的编程语言(Java/Python/Go)就行。