电商大促期间,如何设计"秒杀"架构支撑百万级并发请求?
电商大促期间,如何设计"秒杀"架构支撑百万级并发请求?
每年大促,“秒杀崩了”“抢到货却没订单”“库存越卖越多” 的吐槽总能冲上热搜。看似是 “系统扛不住流量”,实则暴露了秒杀架构的核心矛盾:当瞬时流量远超日常 10-100 倍,如何在 “快” 与 “准”“稳” 之间找到平衡?
今天牛哥就结合多年大促架构实战经验,手把手教你搭出能扛百万级并发的秒杀系统。
先拆需求,别上来就谈技术
做秒杀架构,最忌讳一开口就是 “我要用 Redis Cluster”“我要分库分表”。面试时被问到秒杀架构,面试官第一个想知道的就是:你能不能把模糊的“高并发”拆解成可落地的具体指标?
从用户视角拆业务
从你点 “立即抢购” 到收到成功短信,整个过程能拆成 6 个关键环节,每个环节都有必须遵守的规则,而且环环相扣:
- 商品预热展示:活动开始前1小时,商品详情页要显示“倒计时”“剩余库存”,但此时不能下单——这就需要“预热开关”,避免提前泄露库存。
- 用户资格校验:得先检查用户有没有登录、有没有被拉黑、是不是新用户(有些活动限新),资格不够直接挡在门外。
- 库存检查:这是核心约束,比如“1人只能抢1件”,得用用户ID+商品ID做唯一标识,防止重复下单;“库存售完立即下架”,则要求库存为0时立即关闭下单入口。
- 下单扣库存:扣库存必须是原子操作,不能出现“两个请求同时读到库存=1,都扣减成功,最后库存=-1”的超卖情况。
- 支付确认:支付超时(比如15分钟)要自动释放库存,不然“占着库存不付款”会导致真正想买的用户抢不到。
- 结果通知:支付成功后,短信、App推送得10秒内到,用户等太久会以为“没抢到”而重复下单。
这些流程不是孤立的,比如“资格校验失败”就不该进入库存检查,“库存不足”就不该创建订单。把流程拆细了,才能知道哪里该加拦截、哪里该做异步。
从技术视角定指标
光说“高并发”“高可用”太笼统,得翻译成具体数字。就像医生看病要测体温,系统设计也得有“健康指标”:
并发能力指标:双 11 级别的秒杀,峰值 QPS 得扛住 100 万 — 比如 100 万人同时点抢购,但真正成功下单的 TPS(每秒订单数)可能只有 5 万,剩下 95% 都是无效请求,比如重复点击、资格不符。这里要注意,QPS是“请求总量”,TPS是“有效订单量”,别搞混了。
稳定性指标:用户点抢购后,99% 的请求得在 200ms 内有回应,不然用户会以为“卡了”疯狂刷新,反而加重服务器负担。而且秒杀这种核心场景,故障恢复时间RTO必须≤5分钟,毕竟不能让用户干等半小时吧?
数据一致性指标:库存得 100% 准确,超卖 1 件都可能引发大量投诉。支付和订单状态也得同步快,延迟不能超 10 秒。
安全需求指标:防刷量得拦住 99% 以上的脚本,不然黄牛 10 秒抢光 5000 件库存,真实用户一件都得不到,活动就成了 “给黄牛发福利”。接口也得防篡改,比如商品价格不能被人改成 1 元;黑名单用户必须 100% 拦住,之前有恶意退款、刷量的,绝对不能让他们再参与。
架构搭建:秒杀系统的“四层骨架”
需求明确后,就可以搭架构了。接入层、流量削峰层、业务逻辑层、数据层这四层,每层各司其职,缺一不可。
接入层:先把 “垃圾流量” 拦在门外
用户的请求第一个到的就是接入层,这里要是没拦住无效流量,后面的数据库、业务服务很快就会垮。 为什么选CDN+APISIX?因为CDN能扛静态资源流量,APISIX比Nginx更灵活,适合秒杀这种需要频繁调整规则的场景。
静态资源CDN加速:商品图、活动文案、倒计时动画这些静态内容,全扔到阿里云CDN。在北京访问,就从北京的 CDN 节点拿数据,不用绕回源站挤带宽。实测下来,CDN能分担70%以上的静态资源流量,源站带宽成本直降60%。
精细化限流:用 APISIX 的令牌桶插件,比如单 IP 每秒最多发 5 个请求(防脚本),单用户 1 分钟最多点 10 次(防小号刷),没登录的直接拦住。而且限流不能 “一刀切”,要是正常用户点快了,给个 “操作太频繁,稍后再试” 的提示就行,别直接返回503。
地域路由:华北用户走华北的网关,华东用户走华东的,减少跨地域延迟。
不过要注意,CDN 缓存的页面得设 “预热 + 过期时间”:秒杀前 1 小时把商品详情页预热到 CDN,活动一结束就让缓存过期,避免你看到 “旧库存”。
流量削峰层:把 “流量尖峰” 压平
秒杀流量像海啸,前 10 秒可能集中 80% 的请求,直接打给业务系统,数据库肯定扛不住。这一层的作用就是 “缓冲 + 排队”,把 “10 秒 100 万请求” 变成 “10 分钟 100 万请求”,让系统慢慢处理。
消息队列缓冲:用户下单请求先丢进RocketMQ,业务服务按自己的能力(比如每秒处理5000单)慢慢消费。为什么选RocketMQ?因为它支持事务消息,能保证下单和扣库存同步,还能处理失败的请求,比 Kafka 更适合业务场景。但要注意,消息入队≠秒杀成功,得告诉用户“正在排队,别着急”。
用户排队:用Redis的List结构做排队队列,用户请求进来时,用LPUSH seckill:queue:{productId} {userId}加入队列,再用LLEN seckill:queue:{productId}查排队位置,实时返回给前端“当前排第58位,预计等待2分钟”。用户知道自己在排队,就不会疯狂刷新,能减少30%的无效请求。
积压处理:MQ 积压也是个大问题,得有预案。可以提前制定规则:要是单个 MQ 分区积压超 10 万条,就临时加 2 个消费组(平时只有 1 个),优先处理下单请求,同时暂停“秒杀成功通知”这类非核心消费。
4.3 业务逻辑层:高效处理“核心流程”
这一层是系统的 “大脑”,得又快又准又稳。我们用 Spring Cloud Alibaba,它把 Nacos 服务发现、Sentinel 熔断降级、Dubbo RPC 调用都整合好了,不用自己拼组件;再加个 Caffeine 本地缓存,比 Redis 还快,适合存高频用的小数据。
服务独立拆分:把秒杀拆成3个微服务,各自独立部署、扩容:
- 资格校验服务:查用户是否登录、是否在黑名单、是否已秒杀过;
- 库存扣减服务:处理Redis预扣、MySQL确认扣减;
- 订单生成服务:创建订单、对接支付渠道。
这样即使订单服务挂了,资格校验和库存扣减还能正常工作,不会“一挂全挂”。
内存快速校验:资格校验的规则如用户等级、历史购买记录,全放Caffeine本地缓存,Key是“userId+productId”,Value是“是否有资格”,过期时间30分钟。查的时候先读本地缓存,没查到再读 Redis,最后查数据库,三级校验下来,平均耗时从50ms降到8ms。
热点隔离:热点商品也得单独处理,比如 1 元秒杀手机这种爆款,请求可能占总流量的 30% 以上。给这类商品单独部署服务器,比如其他商品用 20 台,爆款用 50 台,避免 “一个商品拖垮所有服务”。单独隔离后,其他商品的响应延迟稳定在100ms内。
数据层:支撑“高读写”还得“准”
数据层是“弹药库”,既要扛住高读写,又不能出错。用 Redis Cluster存热数据,MySQL存冷数据,Sharding-JDBC做分库分表,分工明确。
Redis存“热数据”:实时库存(seckill:stock:{productId})、用户排队位置(seckill:queue:{productId})、资格校验结果(seckill:qualify:{userId}:{productId})全放Redis。用 3 主 3 从、16 个分片的架构,单个分片 QPS 能到 5 万,16 个就是 80 万,足够扛秒杀。不过 Redis 主从同步有毫秒级延迟,可能主库扣了库存,从库还没更,你查的时候看到的还是旧库存,后面讲缓存优化时会说怎么解决。
MySQL存“冷数据”:MySQL 存订单、支付记录这些要长久保存的数据,用 Sharding-JDBC 按商品 ID 哈希分 8 个库,每个库再分 16 张表,总共 128 张表。比如商品 ID 是 1001,哈希后分到库 0、表 3,这样单表数据量能控制在 50 万以内,要是不分表,单表超 1000 万就慢了,查询速度提升 5 倍。
读写分离:MySQL 主库只负责写(创订单、扣库存),从库负责读(查订单、查支付状态)。用 Sharding-JDBC 的插件,读请求自动分给从库,主库压力能降 40%。但主从同步有 1-3 秒延迟,你刚下单可能查不到订单,得提示 “订单创建中,10 秒后再查”。
关键细节:这些“坑”一定要避开
架构搭好了,还得填“细节坑”。秒杀出问题,往往不是架构不对,而是细节没处理好——比如超卖、缓存雪崩、数据不一致,这些都是面试高频考点,也是线上事故的重灾区。
5.1 防超卖:守住“库存底线”
面试官常问:“怎么保证绝对不超卖?” 这问题看似简单,实则藏着并发编程的“魔鬼细节”。超卖的根源是“多个线程同时读库存、扣库存,导致数据不一致”。选择“Redis预扣+MySQL乐观锁”,兼顾性能和准确性。
Redis预扣库存(Lua脚本原子操作):秒杀开始前,把库存加载到Redis(HSET seckill:stock:{productId} available 1000)。用户下单时,用Lua脚本原子扣减库存,避免并发问题:
-- KEYS[1] = "seckill:stock:{productId}"
-- ARGV[1] = 1(购买数量)
local available = redis.call("HGET", KEYS[1], "available")
if not available or tonumber(available) < tonumber(ARGV[1]) then
return 0 -- 库存不足,返回0
end
redis.call("HINCRBY", KEYS[1], "available", -tonumber(ARGV[1])) -- 扣减库存
return 1 -- 扣减成功,返回1Lua脚本在Redis中是原子执行的,不会被其他请求打断,这是防超卖的“第一道防线”。
MySQL乐观锁最终确认:Redis扣减成功后,得落库才算数。库存表设计时加个version字段(乐观锁),扣减时对比版本号:
CREATE TABLE seckill_stock (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL COMMENT '商品ID',
available_stock INT NOT NULL COMMENT '可用库存',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
UNIQUE KEY uk_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 扣库存SQL
UPDATE seckill_stock
SET available_stock = available_stock - 1, version = version + 1
WHERE product_id = #{productId} AND available_stock >= 1 AND version = #{version};执行后看影响行数:要是 1,说明扣成了;要是 0,说明库存被别人抢光了,得把 Redis 的库存加回来(HINCRBY seckill:stock:1001 available 1)。
库存预分配:如果商品库存10000件,全国分5个仓,华北仓2000、华东仓3000...用户下单时,在北京下单就扣华北仓的,减少跨仓竞争。
5.2 缓存优化:解决穿透、击穿、雪崩
缓存是秒杀的“性能引擎”,但用不好就成“定时炸弹”。穿透、击穿、雪崩这三个问题最常见。
缓存穿透(查不到数据,一直查DB):用户请求不存在的商品ID(比如productId=999999),缓存和DB都没有,请求直接打给DB。解决方案是“布隆过滤器+缓存空值”:
- 秒杀开始前,把所有商品ID加载到布隆过滤器(Redis的BF模块),请求先过过滤器,不存在的直接返回“商品不存在”;
- 万一过滤器有漏网之鱼(布隆过滤器有0.1%误判率),查DB为空后,缓存
null值(SET product:{productId} null EX 300),5分钟内不再查DB。
缓存击穿(热点Key过期,瞬间打DB):某个爆款商品的缓存突然过期(比如1小时过期,刚好到点),10万请求同时打给DB。解决方案是“互斥锁+热点Key永不过期”:
- 用Redisson的分布式锁(比自研的可靠),第一个请求拿到锁后查数据库、更缓存,其他请求等着重试;
RLock lock = redissonClient.getLock("lock:product:" + productId); try { if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 5秒抢锁,30秒自动释放 Product product = productMapper.selectById(productId); redisTemplate.opsForValue().set("product:" + productId, product, 1, TimeUnit.HOURS); return product; } else { return redisTemplate.opsForValue().get("product:" + productId); // 重试读缓存 } } finally { if (lock.isHeldByCurrentThread()) lock.unlock(); } - 热点Key在代码层不设过期时间,用定时任务每 30 分钟异步更新,避免主动过期。
缓存雪崩(大量Key同时过期,DB被压垮):缓存雪崩是指大量缓存同时过期,比如秒杀结束后 1000 个商品的缓存一起失效,数据库扛不住。解决办法是 “过期时间随机化”,给每个缓存的过期时间加个随机值,比如 “1 小时 ±10 分钟”,避免集中过期;另外本地缓存(Caffeine)没过期的话,不会请求 Redis,也能挡一波流量。
分布式一致性:确保“数据不混乱”
分布式系统里,“绝对一致” 是不可能的 —— 网络延迟、节点故障都会导致数据暂时对不上,关键是 “最终要一致”,别出现 “付了钱没订单”“扣了库存没下单” 的情况。
轻量化TCC事务:秒杀下单是短事务,TCC 比 2PC(慢)、SAGA(适合长事务)更合适:Try 阶段查资格、预扣 Redis 库存;Confirm 阶段确认扣 MySQL 库存、创订单;Cancel 阶段回滚 Redis 库存、删排队记录。不过 TCC 要写很多补偿代码,比如 Cancel 失败了要重试。
消息队列最终一致:订单创建后,发事务消息到 RocketMQ,库存服务读消息扣库存。要是扣库存失败,消息会自动重试 3 次,还失败就进死信队列,人工处理。就算订单和库存服务暂时断网,消息也不会丢,最后总能同步上。
实时对账任务:每分钟对比Redis和MySQL的库存差异(Redis库存 - MySQL库存),如果差值超10(可能是Redis扣了没同步到MySQL,或MySQL回滚了Redis没加),就以MySQL为准修复Redis(HSET seckill:stock:{productId} available {MySQL库存})。
监控与容灾:系统“出问题能兜底”
“三分技术,七分运维”,秒杀系统的监控和容灾比架构设计更重要。线上出问题不可怕,可怕的是“出了问题不知道,知道了修不好”。
全链路监控(Prometheus+Grafana+SkyWalking):
- 物理指标:CPU、内存、带宽、磁盘IO(用Node Exporter采集);
- 应用指标:QPS、响应延迟(P50/P90/P99)、错误率、JVM堆内存(用Micrometer埋点);
- 链路追踪:SkyWalking 跟踪你从点击到下单的全链路耗时,哪慢了一眼就看出来。
混沌工程演练:大促前1个月,每周搞1次“故障注入”,故意制造故障测系统:
- 随机下线20%业务实例(看K8s会不会自动扩容替补);
- 关闭1个Redis分片(看主从切换、数据迁移是否正常);
- MQ单分区网络延迟+500ms(看消费会不会积压、业务会不会超时)。
去年演练时,发现“Redis分片下线后,库存服务没切到新分片”,改了路由逻辑后,线上就没出这个问题。
降级预案(开关中心+分级降级):
用 Apollo 配置中心埋好开关,比如 “关商品评价”“关猜你喜欢”“只留华北服务”;还分了三级降级:一级关非核心功能,二级关部分区域服务,三级只保下单。要是 QPS 超了设计值(比如 120 万超 100 万),就手动触发一级降级,把资源让给下单流程。
六、优化方向:从“能用”到“好用”
系统跑起来只是“及格”,要做到“优秀”,还得持续优化。优化不是“炫技”,而是“解决用户痛点、降低成本”——让用户抢得爽、让公司少花钱,这才是优化的价值。
6.1 无效请求提前过滤:减少下游压力
秒杀场景下,90%的请求都是无效的(重复点击、没资格、库存不足),提前过滤能“减负增效”。我们在接入层、应用层各加了过滤规则,效果显著:
| 过滤阶段 | 规则示例 | 过滤效果 |
|---|---|---|
| 接入层 | 单IP每秒>5请求、User-Agent异常(脚本标识) | 挡掉30%无效请求 |
| 应用层 | 未登录用户、黑名单用户、已秒杀用户 | 挡掉50%无效请求 |
比如“已秒杀用户过滤”,用Redis的Set结构记录已秒杀用户(SADD seckill:users:{productId} {userId}),下单前查SISMEMBER seckill:users:{productId} {userId},存在就返回“您已参与过本次秒杀”。重复下单请求能降80%。
6.2 异步处理提速:非核心流程“后台跑”
用户下单后,发短信、推App通知、更新用户积分,这些操作用户“不关心实时性”,完全可以异步处理。我们用RocketMQ的普通消息队列(非事务消息),下单成功后丢消息,后台服务慢慢消费:
// 下单成功后,发异步消息
rocketMQTemplate.send("order-notify-topic", MessageBuilder.withPayload(orderId).build());
// 消费端处理(单独部署,不影响下单流程)
@RocketMQMessageListener(topic = "order-notify-topic", consumerGroup = "notify-group")
public class NotifyConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String orderId) {
sendSms(orderId); // 发短信
pushApp(orderId); // 推App通知
updateUserScore(orderId); // 更新积分
}
}这么一改,下单接口响应时间从 500ms 降到 150ms,你点了就有反馈,体验好了 3 倍。
6.3 跟着流量弹性扩容
云时代了,还手动扩容就太“原始”了。用 K8s 的 HPA 自动扩缩容,资源跟着流量变:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: seckill-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: seckill-service
minReplicas: 2 # 平时2个实例
maxReplicas: 20 # 秒杀时最多20个实例
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU超70%就扩容
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 60秒内不再缩容秒杀前 10 分钟,HPA 会自动扩到 10 个实例;活动结束 5 分钟,CPU 降下来了,又缩回 2 个。既保证性能,又不浪费资源,每月能省 40% 的服务器成本。
七、总结:秒杀架构的“设计心法”
总结下来,秒杀架构设计是 “平衡的艺术”—— 平衡性能与一致、成本与体验、复杂与稳定。从 0 到 1 做秒杀,记住这 4 条心法,能少走 90% 的弯路:
- 流量拦截要“前置”:CDN挡静态、网关限流量、排队筛用户,把无效请求挡在越上游越好,别让它们“走到数据库门口才被拦下”。
- 数据一致是“底线”:Redis原子扣减、MySQL乐观锁、TCC事务、实时对账,这四重保障缺一不可——超卖1件,对用户来说就是“整个活动不可信”。
- 监控容灾“不偷懒”:全链路监控(看得到问题)、混沌演练(预演问题)、降级预案(解决问题),这三件事做扎实了,线上出问题也能“兜底”。
- 持续优化“不停步”:没有“一劳永逸”的秒杀系统,流量变了、业务变了,架构就得跟着变。去年的方案今年可能就不适用,保持敬畏心,持续迭代,才能真正做好秒杀。
最后想说,秒杀架构要把用户体验放在第一位,把数据安全当作底线,这样设计出来的系统,才能真正扛住“双11”的流量洪峰,也才能在面试中“打动面试官”。
