
秒杀系统设计这道题在系统设计面试里的出现频率高得离谱但同时也是翻车重灾区。我带过的候选人也好身边的朋友去面大厂也好十个里头能有七八个在这道题上答得稀碎。最典型的反应就是面试官刚问完立刻条件反射式地吐出“Redis 抗量 MQ 削峰 限流”三板斧再追问一句“库存怎么扣扣完了订单怎么落超卖怎么防”就开始支支吾吾。说白了大部分人把这道题当成了背诵中间件名词的填空题但面试官真正想考察的是你在极端流量场景下做架构取舍、保护数据一致性、设计降级预案的完整能力。这篇文章就围绕这道题从面试官视角出发把秒杀系统的核心难点、库存扣减的正确姿势、一条可落地链路的设计过程以及我在实战中踩过的一些坑完整拆开来讲。适合准备后端/架构方向面试的工程师也适合真正要设计秒杀业务的同学参考。1. 面试官到底在考什么先把题读懂1.1 这道题不是背诵题是取舍题很多候选人把秒杀系统当八股文背一上来就铺开一整套微服务架构、注册中心、配置中心、全链路监控、分布式链路追踪……结果面试官问到的第一个问题就卡住了“你觉得这个系统大概是什么量级QPS 多少库存多少参加人数多少”为什么这个问题重要因为秒杀架构里的每一个选择都和量级强相关。你做一个 1000 人抢 500 件商品的小活动和做一个 100 万人抢 1 万件商品的大促设计完全是两套方案。前者单机 MySQL 乐观锁就能扛住后者才需要上 Redis、MQ、分库分表、风控那一套。一上来不澄清需求就直接堆方案等于告诉面试官你只会背模板没有真正设计过系统。我在面试里最常听到的答法就是Redis 预减库存、MQ 异步下单、数据库最终扣减。这套方向本身没错但几乎所有人都会漏掉三个关键点第一Redis 扣减失败了怎么补偿第二MQ 积压了用户迟迟拿不到订单结果怎么办第三同一个用户短时间反复点击、用脚本刷接口怎么防面试官真正想听的是你有没有想过这些边界问题而不是听你报菜名。1.2 需求澄清应该问什么拿到这道题第一反应不应该是画架构图而是先问清楚几个业务参数。哪怕面试官没有给你任何数字你也可以主动设定一套合理的业务假设然后再基于假设去做设计。这样表达出来的思考过程比直接给结论要高级得多。至少要澄清这几个维度同时参与秒杀的人数规模决定了接入层和网关需要抗多大的流量。商品库存量决定了数据库最终扣减的写压力。预估峰值 QPS决定了 Redis、MQ、DB 的容量规划。对库存一致性的容忍度秒杀场景一般要求零超卖超卖一单都要出事故。是否允许一个用户多次购买如果限制一人一单就需要额外的幂等和去重机制。下单后的支付时效这决定了未支付订单的库存回补策略。举个例子我通常会这样设定假设1 万件商品10 万人参与活动开始后前 10 秒是请求高峰预估峰值 QPS 5 万。基于这个量级你再去画架构每一步都有数据支撑面试官一听就知道你是有真实经验的。2. 秒杀系统的核心矛盾极端流量下的“读”与“写”2.1 流量漏斗每一层都要筛掉一批请求秒杀系统最本质的挑战是瞬间流量尖峰。平时日活几百万的系统可能平均 QPS 只有几千但秒杀开始那一瞬间流量可能飙升到平时的几十倍甚至上百倍。如果所有请求都打到后端服务和数据库再强的机器也扛不住。所以设计的第一个思路就是做一个流量漏斗在每一层都把无效请求筛掉。漏斗从用户侧就开始了。第一步商品详情页、活动页面、倒计时页面全部静态化扔到 CDN 上同一时刻几万人刷页面压力全在 CDN根本不会打到源站。但静态页里的库存数字是会变的这个动态数据不能也塞进 CDN否则用户看到库存还剩 10 件点进去其实已经卖完了体验极差。我的做法是页面框架走 CDN库存数据用前端异步接口去拉轮询间隔拉大到 3 到 5 秒一次避免用户无脑刷新把接口打爆。第二步前端做交互层拦截。抢购按钮点击后立即置灰同一用户 1 秒内只允许提交一次防止用户手动狂点或者脚本并发刷请求。再配合一个答题验证码活动开始瞬间弹出把自动化脚本挡在门外。这些拦截虽然挡不住所有请求但能把无效请求量级砍掉一大截。第三步接入层限流。用户请求到了 Nginx 或者网关之后要做两件事全局 QPS 限流保护后端服务以及用户维度限流防止单用户刷接口。这里有个很容易踩的坑就是按 IP 限流。秒杀场景下很多用户都挤在同一个公司、学校或小区的 NAT 出口后面按 IP 限流会把正常用户误伤一个办公室几十个人只能进来一两个这种设计在秒杀场景肯定是错的。正确做法是以 user_id 或 device_id 作为限流维度再结合一个更粗粒度的全局令牌桶做整体保护。2.2 读多写少读写一定要分离秒杀流量还有一个鲜明特征叫“读多写少”。用户从看到活动到真正下单要经历好几次页面刷新、库存查询、倒计时轮询这些全是读请求可能占整体流量的 90% 以上。而真正扣减库存、创建订单的写请求占比其实很小。所以读请求和写请求必须分开处理不能混在一条链路上。商品详情、活动规则、库存展示这些读请求尽量走 CDN 和 Redis 缓存只有缓存没命中的时候才回源到后端服务。真正涉及库存扣减的写接口收敛成一个极简的“抢购”入口路径越短越好。每多一个耗时的 RPC 调用、多一次数据库查询QPS 能力就大幅下降。我自己在设计写链路的时候会给自己定一个耗时预算从请求进入网关到返回“抢购中”的结果整个链路不能超过 300 毫秒。倒推下来抢购入口只做三件事参数校验、Redis 原子扣减、发送 MQ 消息所有重活全部异步处理。数据库的写入、订单的创建、库存的最终扣减都交给 MQ 消费者慢慢消化。这样用户侧体验是秒回系统侧压力也被削峰填谷了。3. 库存扣减为什么只提 Redis 一定答不完整3.1 三种库存扣减方案横向对比库存扣减是秒杀系统的核心难点也是这道面试题里最容易翻车的地方。我见过太多人脱口而出“用 Redis 扣库存就行了”但再追问一句“Redis 里扣减和数据库怎么保持一致”就回答不上来了。下面把三种主流方案放在一起对比每一种我都实际用过优缺点非常清楚。方案核心思路优点缺点适用场景数据库乐观锁update 语句里带 stock 0 条件实现极简单不会超卖绝对可靠并发高时数据库行锁竞争严重吞吐量有限库存量小、并发量低的场景Redis 预扣减 MQ 异步落库先用 Redis DECR 扣减库存成功后发 MQ 消息消费者异步写数据库Redis 吞吐高能扛住瞬时高峰数据库压力被削峰Redis 和 DB 存在最终一致窗口需要补偿机制流程更复杂并发量大的主流秒杀场景Redis Lua 原子脚本用 Lua 脚本把库存检查和扣减封装成一个原子操作Redis 单线程执行脚本天然防超卖还可以顺便做用户去重依赖 Redis 高可用库存预热和最终落库仍需单独设计大流量秒杀的主流方案数据库乐观锁的写法很简单一条 update 就搞定UPDATE inventory SET stock stock - 1 WHERE product_id #{productId} AND stock 0;如果影响行数为 0说明库存不足抢购失败。这个方案的优势是逻辑直白数据库层面绝对不会超卖。问题是高并发下所有写操作都串行在行锁上数据库连接池很快被占满整体能扛住的 QPS 天花板很低实测单表乐观锁的写吞吐通常只能到几千 TPS。所以面试时可以说它是兜底方案但不能作为主方案。第二种方案Redis 预扣减。活动开始前把库存预热进 Redis用户请求进来直接DECR扣减扣减成功就发 MQ 异步落库。Redis 单机读写轻松到十万级 QPS扛瞬间高峰没问题。但这里有个隐患Redis 扣减成功了MQ 消息消费失败或者数据库写入失败两边就会不一致。所以必须设计对账和补偿机制定时扫描 Redis 和 DB 的差异数据并修复。这个方案能用但你要能讲清楚怎么兜底否则面试官会追问到漏洞。第三种方案Redis Lua 原子脚本也是我推荐在面试里重点讲的方案。它的核心是用 Lua 脚本把“检查库存 扣减库存 记录用户”放到一个原子操作里执行因为 Redis 是单线程模型脚本运行期间不会被其他命令插入所以并发请求不会互相覆盖。这个方案同时解决了超卖和重复抢购两个问题下面单独展开。3.2 Redis Lua既要防超卖也要防重复先看一段实际的 Lua 脚本local stockKey KEYS[1] local userSetKey KEYS[2] local userId ARGV[1] local quantity tonumber(ARGV[2]) local stock tonumber(redis.call(GET, stockKey)) if not stock or stock quantity then return 0 end -- 判断用户是否已经抢购过 local isPurchased redis.call(SISMEMBER, userSetKey, userId) if isPurchased 1 then return 2 end redis.call(DECRBY, stockKey, quantity) redis.call(SADD, userSetKey, userId) return 1脚本返回三种结果1 表示扣减成功0 表示库存不足2 表示用户重复抢购。这样一次原子操作就把超卖和一人一单都防住了。为什么它能防超卖因为 Redis 是单线程处理命令Lua 脚本在DECRBY执行完之前不会有其他命令插进来修改 stock 的值。所以并发 1 万个请求进来都会老老实实地排队执行这段脚本库存扣到 0 之后后续请求全部返回 0。活动结束后Redis 里的库存数据要同步回数据库做最终一致性。这个同步过程我不建议一条条地更新数据库而是用 MQ 批量消费、批量 update或者每天跑一次对账任务把 Redis 的扣减流水和数据库实际扣减结果做比对。同步的时候要注意幂等性同一个用户、同一个活动只能同步一次否则会出现库存多扣的情况。另外还有一个细节如果用户下单后没有及时支付比如 15 分钟超时订单要关闭库存要回补。回补库存时也要先幂等判断不能一个订单被取消两次把库存加多了。这个逻辑放在支付超时的回调里做同时加一层定时任务扫描兜底。4. 从点击到订单一条能落地的完整链路4.1 核心链路分步骤拆解讲完核心方案我把一条完整的秒杀链路从头到尾走一遍。这套链路我在项目里实际部署过每一步都有明确的作用面试时按这个思路讲逻辑会非常清晰。第一步活动预热。活动开始前把商品库存预热到 Redis商品详情页静态化并上传 CDN预热本地的库存缓存后端服务预先把数据库连接池、线程池调到较大值。这一步的核心目的是让流量进来的时候所有资源都处于就绪状态。第二步用户访问活动页。浏览器请求命中 CDN返回静态页面页面里的库存数字由前端每 3 秒轮询一次后端接口。后端接口优先查 Redis 缓存缓存里没有回源数据库同时设置较短的缓存过期时间避免秒杀开始后数据过于陈旧。第三步用户点击抢购。前端按钮置灰同时带上活动 ID、用户 ID 和签名参数发送到后端抢购接口。注意这个接口的 URL 要动态化在活动开始前才下发带签名的限时链接防止脚本提前探测到真实接口。第四步后端进入秒杀服务。网关层做用户维度限流和全局令牌桶限流秒杀服务做参数校验、签名校验然后执行 Redis Lua 脚本扣减库存。脚本返回 1继续走下一步返回 0 或 2直接返回“手慢了”或“您已参与过”。第五步异步下单。扣减成功后把 userId、productId、actId、库存扣减记录封装成消息发送到 MQ。秒杀接口立刻返回“抢购受理中请稍后查看结果”。这时候用户不会干等着前端会进入一个轮询等待页面。第六步MQ 消费者创建订单。消费者从 MQ 拉取消息先去 Redis 或数据库做幂等校验防止同一个用户被消费两次然后创建订单记录、扣减数据库库存、返回订单号。数据库的 update 依然要带stock 0条件兜底防止极端异常下出现超卖。第七步结果通知。消费者处理完后把“成功”或“失败”的状态写入 Redis。用户前端轮询订单状态接口从 Redis 里拿结果成功就跳转到支付页失败则关闭抢购流程。4.2 容量估算拿着数字做设计很多候选人讲链路讲得天花乱坠一问“你凭什么觉得能扛住”就答不上来了。面试中如果你能主动给出容量估算是很加分的。我分享一个常用的简化估算过程。还是用前面的假设1 万件商品10 万人参与前 10 秒是请求高峰。假设从用户看到页面到点击抢购的转化率是 30%也就是 10 秒内有 3 万次抢购请求平均 QPS 3000峰值翻倍算 6000 QPS。抢购接口只执行 Redis 原子脚本和发 MQ单次链路耗时按 50 毫秒估算单机理论吞吐 20000 QPS所以秒杀服务层部署 2 到 3 台机器就可以扛住 6000 QPS每台机器负载不高还有富余。Redis 方面6000 QPS 对 Redis 来说非常轻松。即使算上库存查询、幂等校验等所有读写Redis 单实例也能支撑几万 QPS。瓶颈不在 Redis而在下游 MQ 消费和数据库写入。MQ 本身吞吐极高但消费者写数据库的能力有限。1 万件商品最终会产生 1 万条订单和 1 万次库存更新分散在活动开始后的 1 到 2 分钟内。如果消费者按批量方式处理每秒能消化 200 到 500 笔1 万笔订单大约 30 秒到 1 分钟处理完完全来得及而且对数据库没有冲击。所以整体结论是对于万级库存、十万级参与的秒杀Redis MQ 异步下单这套方案完全够用。如果库存到百万级、参与人数到千万级那才需要再加分库分表、多地多活、甚至更复杂的云上弹性扩缩容。面试时把量级说清楚再基于量级给方案会显得非常专业。4.3 前端和接入层的防刷细节秒杀系统的对抗对象不只是正常用户还有脚本党。我见过不少活动被脚本刷穿库存的案例所以防刷措施一定要在设计链路里就考虑进去。前端和接入层这部分的处理往往是被候选人忽略但是面试官很爱追问的细节。第一秒杀接口动态化。真正的抢购 URL 使用活动 ID、用户 ID、时间戳和签名生成活动开始前几分钟通过页面脚本动态下发。签名使用后端密钥做 HMAC用户无法伪造。这样脚本无法提前爬取固定的接口去刷。很多老系统用固定 URL被脚本预热后活动一开始就瞬间被机器刷完这个坑要避开。第二验证码延迟弹出。活动开始瞬间先不显示验证码等用户点击抢购按钮后再弹出让脚本来不及在流量峰值前预制答案。滑块或者点选验证码都可以核心目的是拖慢自动化工具的节奏。千万不要用纯数字验证码现在的识别服务准确率很高挡不住脚本。第三多层限流配合。接入层全局令牌桶限制整个集群的入口流量应用层用 Redis 滑动窗口做用户维度限流比如每个用户 1 秒最多 1 次抢购请求。再加上设备指纹风控同一设备被检测到大量异常请求时直接拉黑。这样一层层下来大部分无效流量在触达核心的扣库存逻辑之前就被拦截了。5. 高频追问和容易翻车的细节5.1 超卖到底怎么防超卖是秒杀系统里最敏感的问题面试官几乎必问“你怎么保证不超卖”。完整回答应该包含两层主方案是 Redis Lua 原子脚本在扣减库存这一步就挡住并发超卖兜底方案是数据库 update 带stock 0条件即使异步落库出现异常数据库层面也不可能扣成负数。两层都上了超卖才算是真正被堵死。光答“用 Redis 扣减”是不够的面试官会继续追问“Redis 扣减成功了但 MQ 消费失败数据库没有扣减Redis 显示库存为 0实际数据库还有库存这时候怎么处理”正确的思路是这不是超卖问题而是数据一致性问题需要用对账任务定期比对 Redis 扣减流水和数据库实际库存发现不一致就基于数据库的最终结果做修正。能把这个补偿链路讲出来面试官就会认为你真的处理过生产环境的问题。5.2 一人一单怎么限制很多秒杀活动都要求一个用户只能抢一件防止黄牛囤货。这个限制不是加个数据库唯一索引就完事了高并发下要做前置判断。最方便的做法是我前面 Lua 脚本里已经写过的用 Redis Set 记录每个活动已抢购的用户 ID扣库存之前先判断 SISMEMBER。如果用户已经抢过直接返回重复参与不再扣库存。但这里有一点需要小心如果用户把订单取消掉、并且已经回补了库存这个用户还能不能再抢一次业务规则不同实现也不同。有些活动允许取消后重新抢那 SISMEMBER 判断就不能在取消订单时删除用户记录要设计新的活动轮次。这个细节面试时主动提出来会体现出你对业务规则和实现方案之间关系的思考深度。5.3 接口被脚本狂刷限流挡不住怎么办很多系统的限流方案在秒杀开始瞬间就被打穿原因是限流维度选错了。按 IP 限流会误伤正常用户按全局 QPS 限流会导致系统在高峰时把所有用户都挡在外面。正解的层次是这样第一层用户维度限流这是主防线。一个 user_id 在秒杀活动期间只允许一定频率的抢购请求。第二层设备维度风控用户在登录时获取设备指纹异常设备直接风控别让它进到扣库存环节。第三层接口维度全局限流保护系统不被击穿。第四层数据维度去重Redis Set 里已经存在 userId后续请求直接返回。这四层配合下来脚本即使拿到了 URL也会被用户维度限流挡在外面而且由于动态 URL 签名脚本无法提前拼接合法请求。6. 真实项目里踩过的大坑6.1 Redis 单点故障导致活动直接挂掉我之前做第一版秒杀系统的时候图省事只部署了一个 Redis 实例觉得秒杀时间短Redis 扛一下就过去了。结果活动当天 Redis 实例因为内存不足触发了 OOM整个秒杀服务瞬间不可用所有请求都打到异常链路上用户端全是报错。这个事故给我的教训非常大Redis 在秒杀系统里是核心依赖必须做高可用。哪怕你的预算紧张也要至少搞一主一从加哨兵避免单点故障。同时要设计降级方案如果 Redis 不可用秒杀接口直接快速失败返回“活动太火爆”而不是让请求继续穿透到数据库把后端拖垮。秒杀场景里保护系统比追求成功率更重要。6.2 MQ 消费积压用户一直等待结果另一场活动里扣库存跑得很顺畅用户点击后瞬间得到“抢购受理中”但后续订单一直没创建。排查了半天发现是 MQ 消费者的并发线程数配置太低而且消费者内部还要调一个缓慢的商品服务接口一个消息处理耗时到了 1 秒多。秒杀开始后 MQ 瞬间涌入几千条消息消费者处理不过来消息积压越来越多。这个问题的解决方式是消费者内部不调用远程服务只做纯数据库写入远程查询都改成查本地缓存或 Redis同时把消费者线程数调大启用批量消费模式一次拉取和确认一批消息再加上 MQ 消费 lag 的实时监控报警积压超过阈值就自动扩容。秒杀系统里不要把远程调用放在下单消费者的关键路径上这一点很值得记住。6.3 限流维度选错正常用户被误伤还有一次活动上线前压测发现网关限流阈值设太高没触发上线后瞬间流量冲进来网关按 IP 做了限流结果一个办公区的人全部被限住。用户反馈“亲戚朋友都成功了就我们公司的人点不动”。排查后发现被限的 IP 段就是几个大企业出口。后来把限流改成了用户维度加设备维度网关只做全局 QPS 保护这个问题才解决。秒杀场景下限流的目的不是均匀限制所有人而是把单用户异常流量和全局超阈值流量卡住正常用户的体验要在设计限流方案时优先考虑。6.4 面试现场的高质量回答结构最后说回面试本身。如果你被问到“秒杀系统怎么设计”我建议按照下面这个结构来回答既完整又有层次感第一步澄清需求。主动确认并发量、库存量、是否限购、是否允许取消重抢这些业务约束。第二步给一个整体设计。用一到两分钟把链路讲清楚CDN 静态化、网关限流、秒杀服务、Redis 预热、Lua 原子扣减、MQ 削峰、异步下单、数据库兜底。第三步重点讲两个核心难点。超卖怎么防一人一单怎么限制把 Lua 脚本的逻辑讲出来再补充 DB 乐观锁兜底和对账补偿。第四步讲一下你做过的降级和保护性设计。Redis 挂了怎么办MQ 积压怎么办限流维度怎么选这些才是面试官区分新人和有经验工程师的关键。第五步如果面试官追问某个细节比如“你这套方案支持多大的量级”要用前面讲的容量估算方式回答给出具体数字和结论。我个人这些年做高并发系统的体会是秒杀这道题在面试里压中率极高但能答好的人极少原因就在于大家总是记住了技术名词忘记了系统的本质是在有限资源内优雅地扛住峰值。每次你准备说“用 Redis”之前先问问自己如果 Redis 挂了怎么办如果消息积压了怎么办如果数据库被写爆怎么办能把这三个问题答清楚这道题的分数基本就稳稳到手了。