1. 秒杀系统到底难在哪:业务场景与技术挑战
接手这个需求的时候,产品经理给的描述很简单:"首页挂一个秒杀专场,每天10点、20点开两场,每场放三到五个SKU,每个SKU库存50到200件不等,限时十分钟。"听起来不多?但电商业务的诡异之处就在这——平时日活几万的系统,一到秒杀场次,瞬时流量能干到平时的几十倍。
我负责的系统原本是单体应用,订单、库存、用户、商品全部揉在一个Spring Boot工程里,MySQL单库单表硬扛。平时凑合够用,但秒杀这种场景根本顶不住。热词里有人问"Spring Boot微服务架构图",我当时面对的问题恰恰就是:单体怎么拆、按什么维度拆、拆完之后原本一次本地事务调用变成多次RPC调用怎么保证数据一致。
秒杀系统的难度不在业务逻辑本身——它本质上就三个操作:验证资格、扣库存、生成订单。难的是这三个操作发生在同一瞬间,成千上万个请求同时打过来。这不是"能不能写出来"的问题,而是"扛不扛得住、卖完会不会超卖、用户会不会重复下单"的问题。整个项目做下来,我的核心体会是:秒杀系统的架构设计,本质上是把"瞬时高并发"转化为"可控的异步流量"的过程。
这套设计适合谁参考?如果你是中小团队的后端开发,你们的产品也打算上秒杀、抢购、限量活动这类玩法,这篇文章可以帮你少走很多弯路。我下面讲的不是教科书里的标准答案,而是我在真实业务里踩过坑之后沉淀下来的一套可行方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 微服务拆分:为什么必须拆出独立的秒杀链路
2.1 单体架构在秒杀场景下首先崩掉的是哪里
先复盘一下最初的单体架构在压测中是怎么挂掉的。2000个并发线程打过来,第一个扛不住的不是数据库,而是Tomcat的线程池。默认配置下Spring Boot内嵌Tomcat最大线程数是200,意味着同一时刻只能处理200个请求,剩下的全部排队。而秒杀接口里既有库存查询又有订单写入,平均响应时间被拖到3秒以上,队列越积越长,最后雪崩。
第二个瓶颈是数据库连接池。HikariCP默认最大连接数是10,2000个请求同时进来,全部在等连接释放,数据库端一堆Waiting for connection。第三个问题是应用层的内存和GC,订单、库存、用户三张表的大查询把老年代撑爆,Full GC频繁触发,整个应用进入假死状态。
所以第一步不是优化代码,而是拆分。但拆分不是把原来的单体按controller、service、dao拆成三个工程,那是物理拆分,不是架构拆分。合理的做法是按业务域拆,把秒杀相关的逻辑从主链路中隔离出去。
2.2 电商秒杀的域划分与工程落地
我的拆分方案是这样:
- 用户服务:负责登录态校验、用户等级与风控标签查询,独立部署,承接所有业务共用的用户能力。
- 商品服务:维护商品基本信息、秒杀场次配置、秒杀价格,独立部署,提供商品详情查询接口。
- 库存服务:这是秒杀链路的专用服务,维护SKU库存、预扣库存、库存流水,独立部署,使用独立的Redis集群。
- 订单服务:负责创建订单、订单状态流转,独立部署,与库存服务通过MQ解耦。
- 秒杀网关:基于Spring Cloud Gateway,负责秒杀路由、限流、防重复提交,是整个秒杀流量的唯一入口。
这是一个典型的多服务拆分。你可能会问,用户服务和商品服务原本在单体里就存在,为什么要单独再拆?因为秒杀场景对这两个服务的调用量是平时的几十倍,如果不物理隔离,秒杀流量会把普通商城的查询也拖垮。物理隔离之后,即便秒杀链路出了问题,普通的下单、加购、退款仍然可用。
各服务之间通过OpenFeign做同步RPC调用,通过RocketMQ做异步解耦。工程上统一使用Spring Boot 2.7.x + Spring Cloud Alibaba 2021.x版本,注册中心用Nacos,配置中心也用Nacos,网关用Spring Cloud Gateway,熔断降级用Sentinel。
2.3 拆完之后的一个隐性成本:链路变长,超时控制必须重做
拆分之后最大的感受是:以前单体里一个方法调另一个方法,失败了直接抛异常回滚事务就行。现在服务之间是网络调用,就要面对超时、重试、幂等等一系列分布式问题。
以秒杀主流程为例,请求要经过:网关 -> 秒杀网关 -> 库存服务 -> (MQ) -> 订单服务。每一步都可能超时。我当时的做法是给OpenFeign统一配置了连接超时2秒、读超时3秒的阈值,同时用Sentinel配置了QPS维度的流控规则和异常比例的熔断规则。一条链路的超时时间必须严格把控,任何一个环节读超时时间设置过长,都会拖垮整体吞吐。这是拆完之后很多人都忽略的坑。
3. 库存防超卖:从数据库行锁到Lua脚本的演进
3.1 为什么数据库行锁方案在秒杀场景下不可行
库存防超卖是秒杀系统最核心的问题。最简单的方案是数据库更新时加条件:
sql复制UPDATE sku_stock SET stock = stock - 1 WHERE sku_id = 123 AND stock > 0;
这条SQL利用数据库的行锁保证同一时刻只有一个事务能成功扣减,配合stock > 0条件就能防止超卖。这个方案在低并发下完全没问题,但在秒杀场景下,1000个并发请求同时update同一行,数据库的行锁会让这1000个请求串行执行。虽然数据不会出错,但响应时间完全不可控,数据库的QPS也会被锁竞争拖垮。
我压测过这个方案,2000并发下数据库的CPU直接飙到90%以上,平均响应时间超过5秒,大量请求超时。结论是:数据库防超卖只能作为最后的兜底,不能作为秒杀主链路的方案。
