秒杀几乎是高并发系统设计的「综合考试」。它把瞬时洪峰流量、严格防超卖、热点账户、异步履约这几个难题,全部压到一条请求路径上。能把秒杀设计好,基本等于把限流、缓存、消息队列、最终一致性都过了一遍。
所以面试爱问秒杀,因为它能考出候选人对高并发的整体理解。但秒杀不只是面试题,大促、抢购、抢券这些真实业务里它天天发生。这篇把秒杀系统的设计从流量入口到落库,每一层的取舍讲清楚。
图:秒杀核心思路——层层漏斗,让流量到不了 DB
核心矛盾,瞬时洪峰对上强一致防超卖
秒杀的本质矛盾有两个。
图:秒杀流量经 CDN、网关限流、验证码、Redis 预减、MQ 异步五层漏斗,洪峰稀释成涓流再到数据库
瞬时洪峰。平时可能几十 QPS 的接口,秒杀开始那一秒涌入几万甚至几十万 QPS,是日常流量的千倍万倍。如果没有保护,这股流量会瞬间打垮数据库和下游服务。
强一致防超卖。库存 100 件就是 100 件,一件不能多卖。超卖了是资损(卖给用户的东西没有),少卖了是浪费(库存没卖完但用户买不到)。
这两个矛盾是冲突的。防超卖要求强一致,强一致要求加锁或者走数据库事务,而锁和事务在高并发下是性能毒药。要扛洪峰就得少加锁,要防超卖就得加锁。秒杀系统的所有设计,都是在解这个矛盾。
解矛盾的思路就一句话,让流量根本到不了数据库。用层层漏斗把洪峰稀释成涓流,最终到数据库的写入只有几百 TPS,数据库用传统的事务就能保证一致性。
第一层,CDN 和静态化,挡掉 99% 的流量
秒杀页面、商品详情、活动规则,这些内容是静态的,应该全部推到 CDN。用户进来,99% 的请求根本不到你的服务器,CDN 就地返回了。只有真正点「抢购」按钮的那一下,才走动态接口打到后端。
这一层看似简单但极重要。我见过一个团队秒杀挂了,原因就是活动页没静态化,几百万用户刷新页面,每个请求都打到应用服务器查数据库取活动信息,服务器直接被打爆。把页面静态化推 CDN 后,同样流量轻松扛住。
动态的内容(库存数字、用户状态)单独走异步接口,而且要做节流,不要实时刷新库存数字(用户也不需要看到精确的实时库存),定时刷新一次就够。
第二层,网关限流,把洪峰削成可控流量
过了 CDN 的动态请求,到网关这一层要限流。网关是流量的总闸,不做限流等于裸奔。
限流用令牌桶或者漏桶算法。令牌桶允许一定程度的突发(攒着的令牌可以一次性用掉),漏桶则是严格的匀速输出。秒杀场景通常用令牌桶,因为它更灵活,能应对合理的突发流量。
限流要分维度。全局的总 QPS 限流(保护整个系统),单 IP 的频控(防止单点刷),单用户的频控(防黄牛)。多层限流叠加,把恶意流量和超量流量都挡在业务逻辑之外。
蜂控平台做风控,限流也是核心能力。我们用 Lua 脚本在 Redis 上做原子的计数器扣减,等价于一个分布式限流器。秒杀的限流思路一样,区别是秒杀的限流阈值更激进(因为就是要挡掉绝大部分请求)。
第三层,验证码或者答题,人为制造错峰
这一层有两层意义。一是反外挂机器人,机器抢购比人快太多,加个验证码把机器人挡掉一部分。二是人为制造错峰,用户答题速度不同,原本同一秒涌入的流量被拉平成几十秒的持续流量。
验证码的错峰效果是实打实的。一次大促加了滑块验证码,后端峰值直接降了一半多,不是因为挡掉了多少用户,而是流量被均匀分散了。这个效果对系统稳定特别关键。
第四层,Redis 预减库存,原子操作防超卖
这一层是防超卖的核心。库存前置到 Redis,扣减用 Lua 脚本保证原子性。
图:抢购请求经 Redis Lua 原子扣减,成功发 MQ 异步落库,失败直接返回已抢完,DB 无压力
为什么不用 MySQL 直接扣。MySQL 的 UPDATE stock = stock - 1 WHERE id = X AND stock > 0 逻辑上能防超卖,但高并发下行锁激烈,几十个请求同时抢一行,大部分都在等行锁,TPS 上不去。MySQL 扛秒杀的写入瓶颈在几百到一两千 TPS,而秒杀的抢购请求远超这个数。
Redis 的单线程模型反而成了优势。Lua 脚本在 Redis 里是原子执行的,不会被其他命令打断。扣库存的逻辑(判断大于 0 再减 1)写进 Lua,几万 QPS 的扣减都能串行化处理,而且单次执行极快(微秒级)。
local stock = tonumber(redis.call('get', KEYS[1]))if stock and stock > 0 then redis.call('decr', KEYS[1]) return 1 -- 扣减成功else return -1 -- 库存不足end扣减成功的请求,发 MQ 异步创建订单。扣减失败的,直接返回「已抢完」。这样真正到数据库的写入,是 MQ 消费端按自己的节奏消费的,几百 TPS 平滑写入,数据库毫无压力。
这就是「削峰」的全部秘密。用层层漏斗(CDN、限流、验证码、Redis 预减、MQ 异步)把洪峰稀释,每一层都大量丢弃请求,最终到数据库的写入只有几百 TPS。数据库用传统事务保证一致性,Redis 用原子操作保证预减的准确性。
Redis 预减库存和数据库的真实库存之间,有一个短暂的不一致窗口。Redis 扣了但数据库还没落,这段时间如果 Redis 挂了,数据可能丢。生产实践里要处理这个,Redis 持久化加数据库兜底对账。大促级别的秒杀,还要做容灾预案,Redis 集群挂了怎么降级(比如临时切到数据库直接扣,牺牲性能保正确)。防超卖的可靠性,和限流的可用性,要有独立的容灾设计。
异步下单和最终一致
Redis 预减成功后,请求立刻返回「抢购成功,正在处理」,真正的订单创建走 MQ 异步。用户在前端等几秒看到订单生成。
图:异步下单链路用可靠消息防丢失、幂等防重复、失败回补库存,保障最终一致
异步下单要处理几个一致性问题。MQ 消息不能丢,丢了就是用户扣了库存没拿到订单。要用可靠消息(RocketMQ 的事务消息或者本地消息表),保证扣库存和发消息是一个事务。消费端创建订单要幂等,防止消息重投导致重复下单。订单创建失败(比如用户被风控了)要把库存回补。
这一套异步链路是秒杀的履约层,它的可靠性决定了「抢到了但没拿到订单」这类客诉的多少。大促零资损的目标,不光靠防超卖,也靠这套异步链路的可靠兜底。
限流和降级,最后的防线
秒杀必须有限流和降级的预案。万一前面某一层没兜住,流量涌到下游,要有熔断保护核心服务。
库存服务挂了,降级成「暂停抢购」而不是让请求继续打。订单服务超时,熔断后返回「系统繁忙」而不是让用户一直转圈。这些降级开关要提前准备好,关键时候一键切换。
监控是底线。库存余量、Redis 命中率、MQ 堆积、订单创建成功率,这些指标要实时监控。任何一个异常都可能预示着超卖或者系统过载,要能第一时间发现和处理。大促的秒杀,值班和预案是标配,不是可选的。
秒杀系统的设计,精髓不在某一个技巧,在那个「层层削峰」的整体思路。每一层都做一件事,CDN 挡静态、网关限流、验证码错峰、Redis 预减防超卖、MQ 异步落库。每层都大量丢弃请求,最终到数据库的只是涓流。理解了这个漏斗模型,秒杀就不神秘了,它是一组成熟技术(缓存、限流、消息队列、原子操作)的组合运用,考验的是把它们串起来的工程能力。








