秒杀系统是面试中的高频题,因为它能在短时间内把高并发、缓存、消息队列、分布式锁、数据库瓶颈全部串起来。很多人一上来就说“用 Redis 扣库存,然后发 MQ 异步下单”,但面试官真正想听的,是你如何一层层把流量削掉、把系统压住。下面从架构分层的角度,拆解一个可落地的秒杀后端设计。
第一层:入口限流与防刷
秒杀开始瞬间,流量可能是平时的几百倍,绝不能让它直接打到后端。首先在网关层做限流,比如 Nginx 的limit_req或 API 网关的令牌桶,按用户维度、IP 维度、接口维度分别设阈值。同时,秒杀 URL 要动态化,避免被脚本提前拿到;加验证码或答题,把机器流量挡在门外。对于同一用户短时间重复请求,用 Redis 做setnx去重,只放行第一次。这一层的目标是:把 90% 的无效流量消灭在入口。
第二层:缓存预热与库存扣减
库存不能直接查数据库。秒杀开始前,把商品库存、状态、限购数量预热到 Redis。扣库存必须保证原子性,推荐用 Lua 脚本:
local stock = redis.call('get', KEYS[1]) if not stock or tonumber(stock) <= 0 then return 0 end redis.call('decr', KEYS[1]) return 1Lua 脚本在 Redis 中单线程执行,天然避免并发超卖。但单 key 库存会成为热点,如果 QPS 极高,可以分段库存:把 1000 件库存拆成 10 个 key,每个 100 件,请求按用户 ID 哈希到不同 key,分散压力。扣减成功后,记录用户购买标记,防止重复下单。
第三层:消息队列异步下单
Redis 扣减成功不代表订单创建成功。如果同步写数据库,数据库根本扛不住。正确做法是:扣减库存后,立即向 MQ 发送一条消息,内容包含用户 ID、商品 ID、秒杀令牌,然后直接返回“排队中”。下游订单服务消费消息,落库创建订单,再异步通知用户。这样,前端请求的 RT 从几百毫秒降到几毫秒,系统吞吐量大幅提升。
消息队列选型上,RocketMQ 支持事务消息,Kafka 吞吐更高。关键是要保证消息不丢、不重复。生产者用确认机制,消费者做幂等——可以用“用户 ID + 商品 ID”作为唯一键,插入订单前先查重或利用数据库唯一索引。
第四层:数据库最终一致
订单落库后,还需要扣减真实库存。这里不能再用 Redis 的库存,而是数据库的库存字段。由于 MQ 已经削峰,数据库压力可控。采用“乐观锁 + 库存判断”:
UPDATE stock SET count = count 1 WHERE goods_id = ? AND count >= 1;
如果影响行数为 0,说明库存不足,触发回滚:删除 Redis 中的购买标记,补偿库存。为了最终一致,可以引入本地消息表或定时对账,确保 Redis、MQ、数据库三者的状态最终吻合。
第五层:降级与兜底
秒杀系统必须有 Plan B。如果 Redis 挂了,直接降级为“活动繁忙,请稍后再试”,而不是拖垮整个服务。如果 MQ 堆积,可以动态扩容消费者,或临时关闭非核心商品。数据库连接池要设上限,避免慢查询拖死应用。此外,监控告警必不可少:Redis 库存、MQ 堆积量、订单创建成功率、接口 RT 都要实时上报。
面试加分点
面试官往往还会追问:如何防止超卖?——Redis Lua + 数据库乐观锁双重保障。如何防止少卖?——定时对账,补偿未支付订单。如何保证幂等?——唯一索引 + 状态机。如何处理热点 key?——库存分段 + 本地缓存。这些细节能体现你对生产环境的理解。
总结
秒杀系统的核心思想是分层过滤、异步削峰、最终一致。入口限流挡住大部分流量,Redis 原子扣减保证不超卖,MQ 异步下单提升吞吐,数据库乐观锁兜底,降级方案保证可用性。每一层只做自己擅长的事,不把压力传给下一层。面试时,如果你能按这个脉络讲清楚,而不是堆砌技术名词,面试官一定会眼前一亮。