Clipping 微信公众号

阿里二面:秒杀系统设计,从前端到Redis全链路拆解,QPS百万扩容和缓存三兄弟一次讲透

by Fox爱分享 原文 ↗
Created: 2026-07-08

公众号名称:Fox爱分享

作者名称:Fox爱分享

发布时间:2026-07-08 07:00

前两天一个读者找我,说在阿里面试被问到了秒杀系统设计,面了整整一个小时。

回来之后他给我发了一大段文字,中心思想就一句话:被问麻了。

从前端到后端全链路怎么设计,限流怎么做,库存怎么扣,超卖怎么防,QPS从1万涨到100万架构要怎么调,最后还加了个Redis缓存击穿穿透雪崩三连。

他说每个点好像都能答一点,但连在一起讲的时候,脑子里的东西全碎了,东一块西一块的。

我当时回了他一句话:你这个状态太正常了。

秒杀系统本来就是分布式系统设计里最难啃的骨头之一,不是因为单个技术点多高深,而是因为它把高并发、高可用、数据一致性这几个互相矛盾的东西全塞到了一个场景里。你单独拎一个出来都能讲,但把它们拧成一股绳讲出一个完整的方案,那确实需要点功力。

今天我把这个题彻底拆一遍,从前到后串一条完整的线,每一站解决什么问题、为什么这么做、有什么坑,全部讲到位。

如果你也在准备后端面试,这篇建议存下来反复看。


秒杀到底难在哪?

先说一句大实话,秒杀这个事为什么难。

不是因为人多。12306 春运抢票人也多,但那个是持续性的高并发,不是秒杀这种脉冲式的。秒杀真正的难点在于,99% 的流量在一秒钟之内涌进来,然后十秒钟之后又全散了。

你想想看,平时一个电商系统 QPS 可能就几百上千,秒杀那一瞬间直接飙到几十万甚至上百万。你的服务器、数据库、缓存,所有这些基础设施瞬间承受的压力是平时的几百上千倍。

而库存就那么多。一千台手机,一百万人抢。这一千台手机就像一个针眼,一百万人想同时挤过去。

这就是秒杀的终极矛盾:海量的瞬时请求,跟极度有限的资源之间的冲突。

理解了这一点,后面所有的设计你会发现,全是在解决这个冲突。


全链路设计:从前端到数据库

好,现在我们就从请求进来的第一站开始,一步步串起来。

整个链路分四层:前端 → 网关 → 业务 → 数据,每一层的职责很明确。

前端层:挡掉 30%-40% 的无效流量

秒杀开始前,用户盯着那个倒计时按钮,上面的数字一跳一跳的。这个时候前端已经在干活了。

三板斧:按钮置灰、CDN 静态化、动态 URL。

第一招,按钮置灰。秒杀开始前按钮不可点击,不是为了用户体验好,是为了挡住那些写脚本自动狂点的。你别小看这一步,如果按钮不置灰,秒杀开始的一瞬间,一个用户就可能发出几十上百个请求,这些请求全是无效的,白白压在后端服务器上。

第二招,静态资源 CDN。商品图片、CSS、JS 这些东西,在秒杀开始前就应该推到 CDN 上。你不能让用户打开页面的时候,图片还要从你的源站加载。一秒一百万请求的时候,光静态资源就能把你的带宽打满。

第三招,动态 URL。页面上最终提交订单的那个地址,是动态的,秒杀开始前才下发。这是后端给的一个动态 token,目的是防止有人提前拿到 URL 写脚本蹲点。

这三板斧打出去,能挡掉至少 30%-40% 的无效流量。

网关层:限流是第一道闸门

请求过了前端,第一站是网关。

网关这层要干的事情就一个字:

不是所有的请求都应该进到业务系统里。你一台服务器撑死扛几千 QPS,一百万的请求全放进去,业务层直接就崩了。网关要做限流,把流量控制在业务层能承受的范围内。

限流这件事,面试的时候一般会让你说三种策略:

1. 固定窗口计数器

一秒钟最多放 100 个请求,超了就拒。实现最简单,Redis 里 incr 一下就行。但有个很致命的问题:临界点突刺。

比如你在第 0.9 秒的时候发 100 个请求,第 1.1 秒的时候又发 100 个,实际上 0.2 秒内发了 200 个,远远超出了你的限制。所以实际项目里很少单独用。

2. 滑动窗口

把时间窗口切得更细,比如一秒钟切成 10 个小窗口,每 0.1 秒统计一次。这样上一个窗口的尾巴和下一个窗口的头不会叠加在一起,比固定窗口精确很多。

Redis 的 zset 就能实现:每个请求打个时间戳进去,过期了就删掉,统计一下当前窗口内的数量就行。

3. 令牌桶(最常用)

想象有一个桶,你以固定的速度往里面放令牌,比如每秒放 1000 个。每个请求进来都得从桶里拿一个令牌,拿不到就拒绝。

令牌桶的好处是允许一定的突发流量。桶里有令牌的时候,请求能瞬间拿走多个令牌,不会全给你掐死。秒杀这种突发流量场景,令牌桶最合适。

面试的时候如果只让你说一个,就选令牌桶,然后讲清楚它和滑动窗口的区别:令牌桶允许突发,滑动窗口不允许。

另外网关这层还能做一层简单的风控。同一个 IP、同一个用户 ID 短时间内发太多请求,直接拉黑。这个判断逻辑要快,不能去查数据库,用 Redis 的 set 或者 Bloom 过滤器,几毫秒得出结果。

业务层:消息队列削峰 + Redis 原子扣库存

过了网关,请求进入业务系统了。但这还没到真正扣库存的时候。

业务层要干的第一个事,是继续削峰。

消息队列。请求进了业务层先不处理,扔到消息队列里排队。你的业务系统处理能力可能是 5000 QPS,但网关放进来 10000,没关系,队列帮你扛住这多出来的 5000,慢慢消费。

有人说这不就是让用户等着吗?对,就是等着。你返回给用户的页面不是秒杀成功页,是一个排队中页面,上面写着「正在为您排队,前面还有 xxxx 位」。用户虽然不爽,但系统没崩,你还有机会。

队列之后,才是真正的业务处理——扣库存。

这一步是整个秒杀系统的心脏,也是面试官最关注的点。

防超卖:Redis Lua 脚本 + MySQL 乐观锁

库存怎么扣?你第一反应可能是 MySQL 里 update 一下,stock = stock - 1

千万不能这么干。

MySQL 的 update 在 InnoDB 里是行锁,一行记录同时只能有一个事务在操作。一秒几十万个 update 打上去,锁竞争会直接把数据库干废。

正确做法是库存放到 Redis 里,用 Redis 的 decr 原子操作来扣。

Redis 是单线程处理命令的,decr 天然原子,不需要额外加锁。秒杀开始前,把库存数量提前预热到 Redis 里,比如 stock_key = 1000。每来一个请求,执行 decr stock_key,如果返回值大于等于 0,说明库存够,这个用户抢到了。如果返回值小于 0,库存没了。

但这里有一个关键点,面试的时候一定要讲清楚:decr 虽然是原子的,但你「判断 + 扣库存」这个组合操作不是原子的。

什么意思呢?你不能先 get 一下看库存还有没有,再决定要不要 decr。因为在 get 和 decr 之间,可能有别的请求把最后一个库存抢走了。

所以正确的写法是,直接用 decr 的结果来判断。decr 返回大于等于 0,成功;decr 返回小于 0,失败,然后顺手 incr 回去,把扣掉的库存补回来。

但就算这样,decr 之后判断再 incr 回来,这个也不是完全原子的。真正的生产环境里,一般会用 Lua 脚本把整个逻辑包起来,在 Redis 服务端一次性执行完:

local stock = redis.call('decr', KEYS[1])
if stock >= 0 then
    return 1        -- 扣减成功
else
    redis.call('incr', KEYS[1])  -- 补回库存
    return 0        -- 库存不足
end

整个脚本在 Redis 里原子执行,中间不会有其他命令插进来,这才是生产级别的写法

过了 Redis 这关,库存就扣完了?还不够。

Redis 是内存数据库,万一宕机了数据就没了。所以扣完 Redis 之后,还得异步写回 MySQL。这时候 MySQL 压力已经很小了,因为只有抢到的那一千个人才会走到这一步。

MySQL 这层用的是乐观锁

UPDATE stock SET stock = stock - 1 WHERE id = xxx AND stock >= 1

如果 affected rows 是 0,说明库存其实已经没了,回滚前面的操作。

Redis 原子扣减 + MySQL 乐观锁,两层保险,这就是防超卖的标准方案。

扣库存全流程:

数据层:Redis Cluster 分片 + 读写分离

顺着上面的再聊聊缓存。秒杀场景下,Redis 是绝对的主力。商品信息、库存、用户资格校验,全在 Redis 里。MySQL 只做最终落库和订单持久化。

但 Redis 有个问题,它扛的 QPS 也是有上限的。单机 Redis 大概能扛 10 万 QPS,如果再往上呢?

上 Redis Cluster,把数据分片。 比如按商品 ID 哈希,把不同商品的缓存分散到不同的 Redis 节点上,每个节点各扛一部分流量,整体吞吐量就上去了。

而且读和写可以分离。主节点负责写,从节点负责读。秒杀场景读多写少,多挂几个从节点,读流量分摊出去。


QPS 从 1 万到 100 万,架构怎么调?

到这里我已经把一条请求从前端到后端走完了。现在来回答那个升级版的问题:QPS 从 1 万涨到 100 万,你的方案要怎么调?

1 万 QPS:单机就能扛

其实不需要太复杂的架构。一台性能好点的服务器,加个 Redis,加个 MySQL,单机可能就扛住了。网关做简单的令牌桶限流,Redis 管理库存,MySQL 异步落库,够了。

10 万 QPS:上集群

单机不行了。这时候要上集群:

  • 网关层:Nginx 或 OpenResty 多节点负载均衡

  • Redis:集群模式

  • MySQL:主从分离,读写走不同的库

  • 消息队列:Kafka 或 RocketMQ,削峰填谷

100 万 QPS:架构动刀子

光靠堆机器已经不够了,你得在架构上动刀子。

变化一:多级缓存。 本地缓存 + Redis + CDN,三级联动。商品信息这种不怎么变的数据,直接在业务服务器本地用 Caffeine 或 Guava Cache 缓存一份,Redis 都不查了,内存里直接拿,快到飞起。

变化二:读写彻底分离。 这时候 MySQL 的压力不只是扣库存那一步了,订单查询、物流查询这些读操作也会跟着暴涨。需要上专门的搜索引擎,比如 Elasticsearch,把订单数据同步过去,查询走 ES 不走 MySQL。或者更狠的,直接把热数据全放 Redis 里,MySQL 只做冷数据归档。

变化三:业务拆分。 秒杀系统拆成独立的微服务,跟主站电商系统彻底解耦。秒杀的流量再怎么爆,也不会影响正常的商品浏览和下单。

变化四:异地多活。 单个机房的带宽和电力是有物理上限的。百万 QPS 级别,必须考虑就近接入,北京的用户连北京的机房,上海的用户连上海的机房,库存数据通过专线同步。

你看,从 1 万到 100 万,说到底不是技术栈的更换,是架构思路的升级:单机撑不住就集群,集群撑不住就拆分,拆分撑不住就异地。

QPS 量级架构特征核心组件
1 万单机Redis + MySQL + 令牌桶
10 万集群Nginx 负载均衡 + Redis Cluster + MySQL 主从 + MQ
100 万分布式多级缓存 + ES 读写分离 + 微服务拆分 + 异地多活

Redis 缓存三兄弟:击穿、穿透、雪崩

这三个词面试官基本必问,而且很多人容易搞混。

缓存击穿:热点 key 过期

说的是一个热点的 key 过期了,瞬间大量请求直接打到数据库上。 就像一个盾牌上破了一个洞,所有压力都从这个洞漏下去了。

解决方案两个思路:

  1. 互斥锁:第一个请求发现缓存没了,拿锁去查数据库回填缓存,后面的请求等着就行。

  2. 逻辑过期:热点数据不设过期时间,在 value 里存一个逻辑过期时间,用异步线程定期刷新。

缓存穿透:查根本不存在的数据

说的是请求一个数据库里根本不存在的数据,缓存当然也没有,每次请求都穿透缓存命中数据库。 比如有人拿不存在的商品 ID 反复请求。

解决方案也有两个:

  1. 布隆过滤器:把存在的 key 提前存好,请求来了先问布隆这个 key 存不存在,不存在就直接拒了。

  2. 缓存空值:查不到也往 Redis 里塞一个 null,过期时间设短一点,几分钟就行。

缓存雪崩:大量缓存同时过期

说的是大量缓存在同一时间集体过期,请求像雪崩一样全砸到数据库上。

解决方案就是一句话:别让它们同时过期。 给每个 key 的过期时间加一个随机偏移,比如基础过期 1 小时,实际设成 3600 到 4200 秒之间的随机值。或者多级缓存,本地缓存加 Redis 加 DB,每一层都是一个缓冲。

还有一种极端情况,Redis 本身挂了导致的雪崩。这种需要上哨兵模式或者集群模式,保证 Redis 本身的高可用,挂了能自动切。另外加个熔断降级,Redis 挂了就挂了,别让请求全打到 DB 上,宁可先返回一个默认页面,也别把整个系统拖死。

问题触发场景解决方案
缓存击穿单个热点 key 过期互斥锁 / 逻辑过期
缓存穿透查询不存在的数据布隆过滤器 / 缓存空值
缓存雪崩大量 key 同时过期 / Redis 宕机随机 TTL + 多级缓存 + 熔断降级

面试官到底在看什么?

聊了这么多,回到开头那个问题。

你面阿里,面试官问你怎么设计秒杀系统,他在看什么?

他不是在等你背出 Redis 的 decr 命令,也不是在等你默写令牌桶的算法。

他想看的第一件事:你有没有全链路的视野。 能不能从前端一直想到数据库,每一个环节该做什么,为什么这么做。你想不想得到网关要先限流,业务层要先削峰,Redis 扣完库存之后还得用 MySQL 再兜个底。

他想看的第二件事:你的方案有没有弹性。 QPS 翻十倍翻一百倍的时候,你的设计能不能跟着长。是加机器就行的线性扩展,还是需要重构的架构迭代。

还有一件事很重要:你自己知不知道你的方案哪里可能出问题。 Redis 宕机了怎么办,消息队列积压了怎么办,热点商品把某个分片打满了怎么办。一个好的系统设计者,不是给一个完美的方案,是能说清楚每个选择的代价。


总结

秒杀系统设计本质上是考一件事:

在海量瞬时请求和极度有限资源的矛盾下,你能不能从全链路的角度,给出一个既扛得住流量、又保得住数据的方案,并且说清楚每个选择的代价。

面试考点核心方案关键技术
前端拦截按钮置灰 + CDN + 动态 URL挡掉 30%-40% 无效流量
网关限流令牌桶 / 滑动窗口Redis zset / Sentinel
业务削峰消息队列排队Kafka / RocketMQ
防超卖Redis Lua 原子扣减 + MySQL 乐观锁decr + Lua + WHERE stock >= 1
QPS 扩容单机 → 集群 → 拆分 → 异地多活多级缓存 / ES / 微服务
缓存防护击穿 / 穿透 / 雪崩互斥锁 / 布隆过滤器 / 随机 TTL

把这个思路装脑子里,下次面试你就不是东一块西一块地蹦关键词了。你是拉着面试官一起,把你脑子里那条从前端到数据库的请求链路,从头到尾走了一遍。

走完他问的那些问题,你自己心里也有答案了。

系统设计这件事,从来没有人能给你一个拿来就能满分的模板。但有一条清晰的思路,知道从哪开始、经过哪几站、每一站解决什么问题——这就够了。


如果你正在准备大厂后端面试,或者手里的系统设计题总是答得东一块西一块,关注 Fox爱分享,后续持续更新。


内容效果不满意?点此反馈

输入关键词开始搜索