一、秒杀系统的核心痛点与架构设计原则
秒杀是电商场景中最考验架构能力的业务场景之一,其本质是短时高并发冲击下,有限库存资源与海量用户请求的强冲突,同时对数据一致性、系统可用性有极致要求。
核心业务痛点
- 瞬时流量洪峰:秒杀开启瞬间,QPS可能从日常几百飙升至数十万甚至百万级,常规架构会被直接打穿
- 库存超卖风险:多线程并发下,库存的「读-改-写」非原子操作,极易出现库存为负的资损问题
- 读多写少特征:99%以上的请求是活动、库存查询,真正能完成下单的请求仅与库存数量同级
- 恶意请求冲击:黄牛脚本、刷量工具会发起大量无效请求,挤占正常用户的系统资源
- 链路雪崩风险:单环节故障会沿调用链向上传导,最终导致全系统宕机
核心架构设计原则
- 漏斗型流量过滤:将无效请求在链路最前端拦截,越往底层数据层,请求量越少,最大程度保护核心存储
- 读写强分离:读请求全链路走缓存,写请求全链路异步化,避免读写资源竞争
- 一致性优先:宁可少卖、绝不超卖,库存操作必须保证原子性,资损是秒杀系统的最高级故障
- 异步解耦削峰:非核心流程全量异步化,用消息队列将瞬时洪峰平摊到更长时间窗口处理
- 全链路兜底:每一层都必须有限流、熔断、降级方案,极端场景下保证系统不宕机、核心功能可用
二、秒杀系统全链路架构设计

全链路架构遵循「流量逐层收敛」的核心逻辑,从用户端到数据库,每一层都承担对应的流量过滤、请求处理能力,最终只有极少量有效请求能到达数据库层。
三、全链路分层核心优化方案
1. 前端与客户端优化
前端是秒杀流量的第一道防线,核心目标是减少无效请求的发起,从源头降低后端压力。
- 页面全量静态化:将活动页、商品详情页等静态资源全量部署到CDN,避免秒杀时请求源站,动态数据通过接口异步加载
- 用户操作限流:秒杀开启前按钮置灰,开启后限制单次点击,避免用户重复点击产生重复请求;前端限制单用户1秒内最多发起3次请求
- 本地时间校准:秒杀倒计时基于本地时间校准,避免用户频繁请求服务器获取当前时间
- 人机校验前置:秒杀下单前增加滑块、验证码或简单答题环节,将用户请求分散到1-3秒内,大幅降低瞬时QPS,同时拦截机器刷量请求
2. Nginx接入层优化
接入层是流量进入服务端的第一道关口,核心目标是拦截非法请求、限制异常流量,避免无效请求进入后端服务。
- 静态资源本地缓存:将静态资源缓存到Nginx本地,配合CDN实现二级缓存,完全消除静态资源对源站的请求
- IP级限流 :基于
limit_req模块实现IP级限流,比如单IP每秒最多允许10次请求,直接拦截异常IP的洪峰冲击 - 黑白名单管控:基于日志实时分析恶意IP、黄牛IP,直接在接入层拦截,禁止其访问服务
- 非法请求过滤:拦截参数不全、格式错误、请求头异常的非法请求,直接在接入层返回,不转发到后端
- 业务隔离:秒杀请求与普通业务请求使用不同的域名、反向代理规则,避免秒杀流量影响正常业务
3. 网关层优化
网关层是后端服务的统一入口,核心目标是分布式全局限流、请求路由与熔断降级,保护后端业务集群。
- 分布式全局限流:基于Redis+Lua实现滑动窗口全局限流,设置整个秒杀活动的总QPS阈值,超过阈值直接拒绝请求,避免后端集群过载
- 用户维度细粒度限流:基于用户ID实现单用户限流,比如单用户每秒最多5次请求,防止单用户用多IP刷量
- 令牌桶限流算法:采用Resilience4j的令牌桶实现平滑限流,系统以固定速率生成令牌,请求需获取令牌才能被处理,既能应对突发流量,又能控制整体请求速率
- 服务熔断降级:当后端业务集群的异常率、响应超时率超过阈值,自动触发熔断,直接返回友好提示,避免请求持续打向异常服务,引发链路雪崩
- 路径隔离:秒杀接口与普通业务接口使用完全隔离的路由规则,分配独立的线程池处理,避免秒杀请求耗尽网关线程资源
4. 业务层优化
业务层是秒杀核心逻辑的承载层,核心目标是逻辑极简、无状态、异步解耦,最大化提升并发处理能力。
- 业务逻辑极致精简:秒杀下单接口只保留核心校验逻辑,所有非必要逻辑(如用户详情、商品详情查询)全部前置到缓存预热,接口内不做任何多余的数据库查询
- 无状态水平扩容:业务服务完全无状态,所有状态数据都存储在分布式缓存中,可通过水平扩容节点线性提升并发处理能力
- 资格校验全前置:将用户登录校验、活动时间校验、参与资格校验、重复下单校验全部放在业务逻辑最前端,不符合条件的请求直接返回,不执行后续逻辑
- 线程池资源隔离:秒杀业务使用独立的线程池,与普通业务线程池完全隔离,避免秒杀业务耗尽线程资源,影响正常业务运行
- 非核心流程全异步:短信通知、日志记录、用户积分更新等非核心流程,全部通过消息队列异步处理,不占用主线程资源
5. 缓存层(Redis)优化
缓存层是秒杀系统的核心支柱,99%以上的请求都应该在缓存层被处理,核心目标是高并发读写、原子操作、数据一致性。
- 数据提前预热:秒杀开启前1-2小时,将活动信息、商品库存、用户白名单等数据全量预热到Redis,避免秒杀时出现缓存未命中,请求穿透到数据库
- 库存原子操作:基于Redis单线程模型,使用Lua脚本实现库存预扣减的原子操作,保证并发场景下库存不会超卖,同时避免库存出现负数
- 多级缓存架构:采用「本地Caffeine缓存+Redis分布式缓存」的二级缓存架构,热点活动、商品数据放在本地缓存,完全消除Redis的热点key压力
- 缓存穿透防护:对不存在的活动ID、商品ID,直接在网关层拦截;对合法但不存在的数据,缓存空值(过期时间设置为30秒),避免请求持续穿透到数据库
- 缓存击穿防护:热点key不设置过期时间,活动结束后手动删除;对需要设置过期时间的key,采用互斥锁控制,避免key失效瞬间大量请求打到数据库
- 缓存雪崩防护:不同key的过期时间设置随机偏移量,避免大量key同时失效;Redis集群采用主从+哨兵+分片部署,避免单点故障导致整个缓存集群不可用
6. 消息队列层优化
消息队列是秒杀系统的核心削峰组件,核心目标是异步解耦、削峰填谷,将瞬时洪峰转化为后端可处理的平稳流量。
- 削峰填谷:将同步的下单请求转化为异步消息,无论前端有多少请求,后端消费端只按照数据库能承载的速率消费,完全消除数据库的瞬时写入压力
- 异步解耦:订单创建、库存扣减、支付通知、物流通知等环节通过消息队列解耦,避免同步调用的级联失败,单环节故障不影响全链路
- 可靠消费保障:采用消息重试机制,消费失败的消息自动重试,保证订单创建的最终一致性;重试多次失败的消息进入死信队列,人工介入处理,避免消息丢失
- 幂等性处理:所有消息消费都基于请求唯一ID做幂等校验,避免消息重复消费导致的重复下单、重复扣减库存问题
- 顺序消息控制:同一个用户的下单消息采用顺序消息,保证先发起的请求先处理,避免乱序导致的业务异常
7. 数据层(MySQL)优化
数据层是秒杀系统的最终兜底,核心目标是高并发写入、数据一致性、高可用,保证最终数据的准确可靠。
- 读写分离架构:读请求全部走从库,写请求只走主库,大幅降低主库的查询压力,主库只负责核心的库存扣减、订单写入操作
- 行级锁优化 :库存扣减采用
UPDATE ... WHERE ...的行级锁,只锁住当前商品的库存记录,避免表锁,大幅提升并发写入能力 - 分库分表设计:订单表基于用户ID做分库分表,将订单数据分散到多个库、多个表中,提升订单的写入和查询性能,避免单表数据量过大
- 索引精准优化:所有查询字段都建立合适的索引,避免全表扫描;订单表建立用户ID、商品ID、活动ID的普通索引,建立订单号、请求ID的唯一索引,保证查询和幂等校验的性能
- 连接池参数优化:合理设置数据库连接池的核心参数,最大连接数设置为数据库能承载的最优值,避免连接数过多导致数据库性能下降
- 表结构极简设计:表结构只保留核心字段,避免大字段、冗余字段;采用InnoDB引擎,字符集使用utf8mb4,保证数据存储的性能和兼容性
四、秒杀核心难题的底层解决方案
1. 库存超卖问题
超卖是秒杀系统最严重的故障,其根本原因是并发场景下,库存的「读-改-写」操作不是原子操作,多个线程同时读取到相同的库存值,都执行扣减操作,最终导致库存为负。
方案1:数据库悲观锁
基于MySQL的SELECT ... FOR UPDATE行级锁,锁住库存记录,同一时间只有一个线程能读取和扣减库存,完全避免超卖。
sql
BEGIN;
SELECT available_stock FROM seckill_goods WHERE id = 1 FOR UPDATE;
UPDATE seckill_goods SET available_stock = available_stock - 1 WHERE id = 1 AND available_stock > 0;
COMMIT;
- 优势:实现简单,强一致性,绝对不会超卖
- 劣势:锁竞争激烈,并发能力低,高并发下数据库压力极大,容易出现死锁
- 适用场景:低并发秒杀场景,或极端场景下的兜底方案
方案2:数据库乐观锁
给库存表增加version版本号字段,每次更新时校验版本号,只有版本号匹配才能更新成功,保证同一时间只有一个线程能扣减成功。
ini
UPDATE seckill_goods
SET available_stock = available_stock - 1, version = version + 1
WHERE id = 1 AND version = ? AND available_stock > 0;
- 优势:无锁设计,并发能力高于悲观锁,不会出现死锁
- 劣势:高并发下大量更新失败,用户体验差,数据库写入压力依然很大
- 适用场景:中低并发秒杀场景
方案3:Redis原子预扣减+数据库最终扣减(生产级主流方案)
基于Redis单线程模型,用Lua脚本实现库存预扣减的原子操作,只有预扣减成功的请求才能进入后续下单流程,99%的无效请求直接在缓存层拦截,不会落到数据库。
核心Lua脚本(原子预扣减):
lua
local stock = redis.call('GET', KEYS[1])
if stock == false then
return -1
end
if tonumber(stock) <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
return 1
-
执行逻辑:先查询库存,库存不存在返回-1,库存不足返回0,库存充足则原子扣减,返回1
-
原子性保障:Redis执行Lua脚本时,不会被其他命令打断,完全保证操作的原子性,绝对不会出现超卖
-
全流程逻辑:
- 秒杀前将库存预热到Redis
- 用户下单时,执行Lua脚本预扣减库存,扣减失败直接返回库存不足
- 预扣减成功,发送下单消息到消息队列,返回用户排队中
- 消费端异步消费消息,扣减数据库库存,创建订单
- 订单创建失败,回补Redis库存,保证数据最终一致性
-
优势:并发能力极高,Redis单节点可扛10万+QPS,绝大部分请求在缓存层拦截,数据库压力极小,完全杜绝超卖
-
适用场景:高并发秒杀的生产级核心方案
2. 分布式限流算法对比与选型
限流是秒杀系统保护自身的核心手段,核心目标是将请求量控制在系统能承载的范围内,避免系统被洪峰打垮。
| 算法类型 | 核心原理 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 固定窗口限流 | 将时间划分为固定窗口,每个窗口内设置最大请求数,超过则限流 | 实现简单,占用资源少 | 存在临界问题,两个窗口交界处可能出现双倍流量冲击 | 简单的粗粒度限流场景 |
| 滑动窗口限流 | 将固定窗口划分为多个小格子,每次计算当前时间往前一个窗口内的总请求数,超过则限流 | 解决了固定窗口的临界问题,限流精度高 | 实现相对复杂,占用资源更多 | 网关层的精准全局限流 |
| 漏桶算法 | 请求进入漏桶,漏桶以固定速率流出请求,超过桶容量的请求直接被拒绝 | 严格控制请求速率,流量绝对平滑 | 无法应对突发流量,桶满时正常请求也会被拒绝 | 接入层的IP级限流 |
| 令牌桶算法 | 系统以固定速率往桶里放令牌,请求需获取令牌才能处理,桶有最大容量 | 既能平滑限流,又能应对突发流量,灵活性高 | 实现相对复杂 | 业务层的接口级限流 |
生产级秒杀系统中,网关层采用滑动窗口实现全局限流 ,业务层采用令牌桶实现接口级限流 ,接入层采用漏桶实现IP级限流,形成三层限流防护体系。
3. 数据一致性保障
秒杀系统的一致性核心是库存数据的一致性,即Redis预扣库存与数据库实际库存的一致性,订单创建与库存扣减的一致性,采用「最终一致性」模型,优先保证不超卖,再保证数据最终一致。
核心保障方案:
- 消息可靠消费:采用RocketMQ的事务消息,保证Redis预扣减与消息发送的原子性,要么都成功,要么都失败,避免预扣减成功但消息未发送导致的库存冻结
- 库存回补机制:消费端创建订单失败时,必须原子回补Redis库存,同时清除用户的下单标记,避免库存永久冻结
- 幂等性全链路覆盖:所有请求、消息都基于唯一请求ID做幂等校验,避免重复请求、重复消费导致的重复扣减库存
- 定时对账校准:每日凌晨执行定时对账任务,对比Redis库存与数据库库存,以数据库实际库存为准,修正Redis库存,解决长期运行中的数据不一致问题
- 兜底事务控制:数据库的库存扣减与订单创建放在同一个本地事务中,要么都成功,要么都回滚,保证数据库层面的强一致性
4. 高可用兜底方案
秒杀系统必须做到「极端场景下不宕机,核心功能可用」,全链路每一层都必须有兜底方案。
- 服务熔断降级:基于Resilience4j实现服务熔断,当服务异常率、响应超时率超过阈值,自动触发熔断,直接返回友好提示,避免级联故障;系统压力过大时,关闭非核心功能,只保留秒杀下单、订单查询核心接口,释放系统资源
- 集群弹性扩容:提前准备好弹性扩容方案,基于监控数据,当QPS、CPU使用率超过阈值时,自动扩容业务集群、网关集群、Redis分片,线性提升系统处理能力
- 多级兜底限流:前端、Nginx、网关、业务层、Redis、数据库每一层都设置最大QPS阈值,超过阈值直接拒绝请求,保证每一层都不会被打垮,形成全链路防护
- 缓存降级兜底:当Redis集群出现故障,直接切换到数据库悲观锁方案,虽然并发能力下降,但保证秒杀核心功能可用,不会完全瘫痪
- 全链路监控告警:对全链路的QPS、响应时间、异常率、库存数量、消息堆积量等核心指标做实时监控,设置多级告警阈值,出现异常立即通知运维人员介入,提前规避故障
五、代码实现
1. 数据库表结构(MySQL 8.0)
typescript
CREATE TABLE `seckill_activity` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
`activity_name` varchar(128) NOT NULL COMMENT '活动名称',
`start_time` datetime NOT NULL COMMENT '活动开始时间',
`end_time` datetime NOT NULL COMMENT '活动结束时间',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '活动状态:0-未开始,1-进行中,2-已结束',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_status_time` (`status`,`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='秒杀活动表';
CREATE TABLE `seckill_goods` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`goods_name` varchar(128) NOT NULL COMMENT '商品名称',
`original_price` decimal(10,2) NOT NULL COMMENT '原价',
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价',
`total_stock` int NOT NULL COMMENT '总库存',
`available_stock` int NOT NULL COMMENT '可用库存',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_activity_id` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='秒杀商品表';
CREATE TABLE `seckill_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单编号',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`goods_id` bigint NOT NULL COMMENT '商品ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已取消,3-已完成',
`request_id` varchar(64) NOT NULL COMMENT '请求唯一ID,用于幂等',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
UNIQUE KEY `uk_request_id` (`request_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_goods_id` (`goods_id`),
KEY `idx_activity_id` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='秒杀订单表';
2. 项目依赖配置(Maven)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>seckill-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>seckill-demo</name>
<description>秒杀系统demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<rocketmq.version>2.2.3</rocketmq.version>
<guava.version>32.1.3-jre</guava.version>
<fastjson2.version>2.0.49</fastjson2.version>
<resilience4j.version>2.2.0</resilience4j.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>${resilience4j.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. 核心配置文件(application.yml)
yaml
server:
port: 8080
spring:
application:
name: seckill-demo
datasource:
url: jdbc:mysql://localhost:3306/seckill_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: localhost
port: 6379
password: ""
database: 0
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
max-wait: 1000ms
rocketmq:
name-server: localhost:9876
producer:
group: seckill_producer_group
send-message-timeout: 3000
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
resilience4j:
ratelimiter:
instances:
seckillRateLimiter:
limit-for-period: 10000
limit-refresh-period: 1s
timeout-duration: 0s
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
4. 核心实体类
kotlin
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 秒杀活动实体类
* @author ken
*/
@Data
@TableName("seckill_activity")
@Schema(description = "秒杀活动实体")
public class SeckillActivity {
@TableId(type = IdType.AUTO)
@Schema(description = "活动ID")
private Long id;
@Schema(description = "活动名称")
private String activityName;
@Schema(description = "活动开始时间")
private LocalDateTime startTime;
@Schema(description = "活动结束时间")
private LocalDateTime endTime;
@Schema(description = "活动状态:0-未开始,1-进行中,2-已结束")
private Integer status;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
kotlin
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 秒杀商品实体类
* @author ken
*/
@Data
@TableName("seckill_goods")
@Schema(description = "秒杀商品实体")
public class SeckillGoods {
@TableId(type = IdType.AUTO)
@Schema(description = "商品ID")
private Long id;
@Schema(description = "活动ID")
private Long activityId;
@Schema(description = "商品名称")
private String goodsName;
@Schema(description = "原价")
private BigDecimal originalPrice;
@Schema(description = "秒杀价")
private BigDecimal seckillPrice;
@Schema(description = "总库存")
private Integer totalStock;
@Schema(description = "可用库存")
private Integer availableStock;
@Schema(description = "乐观锁版本号")
private Integer version;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
kotlin
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 秒杀订单实体类
* @author ken
*/
@Data
@TableName("seckill_order")
@Schema(description = "秒杀订单实体")
public class SeckillOrder {
@TableId(type = IdType.AUTO)
@Schema(description = "订单ID")
private Long id;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "活动ID")
private Long activityId;
@Schema(description = "商品ID")
private Long goodsId;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "订单金额")
private BigDecimal orderAmount;
@Schema(description = "订单状态:0-待支付,1-已支付,2-已取消,3-已完成")
private Integer orderStatus;
@Schema(description = "请求唯一ID,用于幂等")
private String requestId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
5. 数据访问层(Mapper)
java
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SeckillActivity;
import org.apache.ibatis.annotations.Mapper;
/**
* 秒杀活动Mapper
* @author ken
*/
@Mapper
public interface SeckillActivityMapper extends BaseMapper<SeckillActivity> {
}
less
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SeckillGoods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 秒杀商品Mapper
* @author ken
*/
@Mapper
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
/**
* 扣减商品库存
* @param goodsId 商品ID
* @param num 扣减数量
* @return 影响行数
*/
@Update("UPDATE seckill_goods SET available_stock = available_stock - #{num} WHERE id = #{goodsId} AND available_stock >= #{num}")
int deductStock(@Param("goodsId") Long goodsId, @Param("num") Integer num);
/**
* 回补商品库存
* @param goodsId 商品ID
* @param num 回补数量
* @return 影响行数
*/
@Update("UPDATE seckill_goods SET available_stock = available_stock + #{num} WHERE id = #{goodsId}")
int rollbackStock(@Param("goodsId") Long goodsId, @Param("num") Integer num);
}
java
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SeckillOrder;
import org.apache.ibatis.annotations.Mapper;
/**
* 秒杀订单Mapper
* @author ken
*/
@Mapper
public interface SeckillOrderMapper extends BaseMapper<SeckillOrder> {
}
6. 业务服务层核心实现
java
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.SeckillActivity;
import com.jam.demo.entity.SeckillGoods;
import com.jam.demo.mapper.SeckillActivityMapper;
import com.jam.demo.service.SeckillActivityService;
import com.jam.demo.service.SeckillGoodsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* 秒杀活动服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillActivityServiceImpl extends ServiceImpl<SeckillActivityMapper, SeckillActivity> implements SeckillActivityService {
private final StringRedisTemplate stringRedisTemplate;
private final SeckillGoodsService seckillGoodsService;
private static final String ACTIVITY_KEY_PREFIX = "seckill:activity:";
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
@Override
public void preheatActivity(Long activityId) {
log.info("开始预热活动数据,activityId:{}", activityId);
SeckillActivity activity = this.getById(activityId);
if (ObjectUtils.isEmpty(activity)) {
log.error("活动不存在,activityId:{}", activityId);
throw new RuntimeException("活动不存在");
}
stringRedisTemplate.opsForValue().set(ACTIVITY_KEY_PREFIX + activityId, String.valueOf(activity.getStatus()));
LambdaQueryWrapper<SeckillGoods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SeckillGoods::getActivityId, activityId);
List<SeckillGoods> goodsList = seckillGoodsService.list(queryWrapper);
for (SeckillGoods goods : goodsList) {
stringRedisTemplate.opsForValue().set(STOCK_KEY_PREFIX + goods.getId(), String.valueOf(goods.getAvailableStock()));
log.info("预热商品库存,goodsId:{}, stock:{}", goods.getId(), goods.getAvailableStock());
}
log.info("活动数据预热完成,activityId:{}", activityId);
}
}
ini
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.SeckillOrder;
import com.jam.demo.mapper.SeckillOrderMapper;
import com.jam.demo.request.SeckillRequest;
import com.jam.demo.service.SeckillGoodsService;
import com.jam.demo.service.SeckillOrderService;
import com.alibaba.fastjson2.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.UUID;
/**
* 秒杀订单服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper, SeckillOrder> implements SeckillOrderService {
private final StringRedisTemplate stringRedisTemplate;
private final RocketMQTemplate rocketMQTemplate;
private final SeckillGoodsService seckillGoodsService;
private final SeckillOrderMapper seckillOrderMapper;
private final DataSourceTransactionManager transactionManager;
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
private static final String ORDER_USER_KEY_PREFIX = "seckill:order:user:";
private static final String SECKILL_TOPIC = "seckill_order_topic";
private static final DefaultRedisScript<Long> DEDUCT_STOCK_SCRIPT;
static {
DEDUCT_STOCK_SCRIPT = new DefaultRedisScript<>();
DEDUCT_STOCK_SCRIPT.setScriptText("""
local stock = redis.call('GET', KEYS[1])
if stock == false then
return -1
end
if tonumber(stock) <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
return 1
""");
DEDUCT_STOCK_SCRIPT.setResultType(Long.class);
}
@Override
public String doSeckill(SeckillRequest request) {
String requestId = request.getRequestId();
Long userId = request.getUserId();
Long goodsId = request.getGoodsId();
Long activityId = request.getActivityId();
if (!StringUtils.hasText(requestId)) {
return "请求ID不能为空";
}
if (ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(goodsId) || ObjectUtils.isEmpty(activityId)) {
return "请求参数不完整";
}
String userOrderKey = ORDER_USER_KEY_PREFIX + activityId + ":" + userId;
Boolean isOrdered = stringRedisTemplate.opsForSet().isMember(userOrderKey, String.valueOf(goodsId));
if (Boolean.TRUE.equals(isOrdered)) {
return "您已参与该商品秒杀,请勿重复下单";
}
String stockKey = STOCK_KEY_PREFIX + goodsId;
Long result = stringRedisTemplate.execute(DEDUCT_STOCK_SCRIPT, Collections.singletonList(stockKey));
if (ObjectUtils.isEmpty(result) || result <= 0) {
return "商品库存不足,秒杀失败";
}
try {
rocketMQTemplate.syncSend(SECKILL_TOPIC, JSON.toJSONString(request));
stringRedisTemplate.opsForSet().add(userOrderKey, String.valueOf(goodsId));
log.info("秒杀请求发送成功,userId:{}, goodsId:{}, requestId:{}", userId, goodsId, requestId);
return "秒杀排队中,请稍后查询订单状态";
} catch (Exception e) {
log.error("秒杀消息发送失败,userId:{}, goodsId:{}, requestId:{}", userId, goodsId, requestId, e);
stringRedisTemplate.opsForValue().increment(stockKey);
return "系统繁忙,请稍后再试";
}
}
@Override
public boolean createOrder(SeckillRequest request) {
Long userId = request.getUserId();
Long goodsId = request.getGoodsId();
String requestId = request.getRequestId();
SeckillOrder existOrder = seckillOrderMapper.selectOne(
new LambdaQueryWrapper<SeckillOrder>().eq(SeckillOrder::getRequestId, requestId)
);
if (!ObjectUtils.isEmpty(existOrder)) {
log.warn("重复的下单请求,requestId:{}", requestId);
return true;
}
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
boolean deductSuccess = seckillGoodsService.deductStock(goodsId, 1);
if (!deductSuccess) {
log.error("数据库扣减库存失败,goodsId:{}", goodsId);
transactionManager.rollback(status);
return false;
}
SeckillOrder order = new SeckillOrder();
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
order.setActivityId(request.getActivityId());
order.setGoodsId(goodsId);
order.setUserId(userId);
order.setOrderAmount(seckillGoodsService.getById(goodsId).getSeckillPrice());
order.setOrderStatus(0);
order.setRequestId(requestId);
order.setCreateTime(LocalDateTime.now());
order.setUpdateTime(LocalDateTime.now());
seckillOrderMapper.insert(order);
transactionManager.commit(status);
log.info("秒杀订单创建成功,orderNo:{}, userId:{}, goodsId:{}", order.getOrderNo(), userId, goodsId);
return true;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("秒杀订单创建失败,userId:{}, goodsId:{}, requestId:{}", userId, goodsId, requestId, e);
return false;
}
}
@Override
public SeckillOrder getUserOrder(Long userId, Long goodsId) {
return seckillOrderMapper.selectOne(
new LambdaQueryWrapper<SeckillOrder>()
.eq(SeckillOrder::getUserId, userId)
.eq(SeckillOrder::getGoodsId, goodsId)
.orderByDesc(SeckillOrder::getCreateTime)
.last("LIMIT 1")
);
}
}
7. 消息消费者实现
ini
package com.jam.demo.mq;
import com.jam.demo.request.SeckillRequest;
import com.jam.demo.service.SeckillOrderService;
import com.alibaba.fastjson2.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
/**
* 秒杀订单消息消费者
* @author ken
*/
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(topic = "seckill_order_topic", consumerGroup = "seckill_order_consumer_group")
public class SeckillOrderConsumer implements RocketMQListener<String> {
private final SeckillOrderService seckillOrderService;
private final StringRedisTemplate stringRedisTemplate;
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
private static final String ORDER_USER_KEY_PREFIX = "seckill:order:user:";
@Override
public void onMessage(String message) {
log.info("收到秒杀订单消息,message:{}", message);
SeckillRequest request = JSON.parseObject(message, SeckillRequest.class);
if (ObjectUtils.isEmpty(request)) {
log.error("消息格式错误,message:{}", message);
return;
}
boolean createSuccess = seckillOrderService.createOrder(request);
if (!createSuccess) {
String stockKey = STOCK_KEY_PREFIX + request.getGoodsId();
String userOrderKey = ORDER_USER_KEY_PREFIX + request.getActivityId() + ":" + request.getUserId();
stringRedisTemplate.opsForValue().increment(stockKey);
stringRedisTemplate.opsForSet().remove(userOrderKey, String.valueOf(request.getGoodsId()));
log.error("订单创建失败,已回补库存,goodsId:{}, userId:{}", request.getGoodsId(), request.getUserId());
}
}
}
8. 接口控制器实现
kotlin
package com.jam.demo.controller;
import com.jam.demo.entity.SeckillOrder;
import com.jam.demo.request.SeckillRequest;
import com.jam.demo.response.Result;
import com.jam.demo.service.SeckillActivityService;
import com.jam.demo.service.SeckillOrderService;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 秒杀接口控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/seckill")
@RequiredArgsConstructor
@Tag(name = "秒杀接口", description = "秒杀系统核心接口")
public class SeckillController {
private final SeckillOrderService seckillOrderService;
private final SeckillActivityService seckillActivityService;
private static final String RATE_LIMITER_NAME = "seckillRateLimiter";
@PostMapping("/do")
@Operation(summary = "秒杀下单", description = "用户发起秒杀下单请求")
@RateLimiter(name = RATE_LIMITER_NAME, fallbackMethod = "seckillFallback")
public Result<String> doSeckill(@Valid @RequestBody SeckillRequest request) {
String result = seckillOrderService.doSeckill(request);
return Result.success(result, null);
}
@GetMapping("/order")
@Operation(summary = "查询秒杀订单", description = "查询用户的秒杀订单状态")
public Result<SeckillOrder> getUserOrder(@RequestParam Long userId, @RequestParam Long goodsId) {
SeckillOrder order = seckillOrderService.getUserOrder(userId, goodsId);
return Result.success(order);
}
@PostMapping("/preheat/{activityId}")
@Operation(summary = "预热活动数据", description = "秒杀开始前预热活动和商品库存数据到缓存")
public Result<Void> preheatActivity(@PathVariable Long activityId) {
seckillActivityService.preheatActivity(activityId);
return Result.success(null);
}
public Result<String> seckillFallback(SeckillRequest request, Exception e) {
log.warn("秒杀请求触发限流,userId:{}, goodsId:{}", request.getUserId(), request.getGoodsId(), e);
return Result.fail(429, "当前活动太火爆,请稍后再试");
}
}
9. 项目启动类
kotlin
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 秒杀系统启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class SeckillDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillDemoApplication.class, args);
}
}
10. 秒杀下单核心流程图

六、压测验证与线上运维规范
1. 压测验证核心指标
- QPS:单节点秒杀接口QPS需达到1万+,集群可线性提升
- 响应时间:接口平均响应时间需控制在50ms以内,99分位响应时间控制在200ms以内
- 超卖校验:压测结束后,数据库订单数量不得超过商品总库存,库存不得出现负数
- 一致性校验:Redis库存与数据库可用库存必须一致,无库存冻结、无数据不一致
- 异常率:压测过程中接口异常率必须为0,系统无宕机、无OOM、无Full GC频繁问题
2. 线上运维核心规范
- 提前预热:秒杀开启前2小时完成数据预热,提前扩容集群节点,开启全链路监控
- 灰度发布:秒杀相关的系统变更,必须提前3天完成灰度发布,验证无问题后全量上线
- 全链路压测:每次大促秒杀前,必须完成全链路压测,验证系统的最大承载能力,预留30%以上的冗余容量
- 实时监控告警:对QPS、响应时间、异常率、库存数量、消息堆积量、CPU、内存、磁盘IO等核心指标做实时监控,设置多级告警阈值,出现异常立即通知相关人员
- 容灾演练:定期进行容灾演练,模拟Redis宕机、消息队列宕机、数据库宕机等极端场景,验证兜底方案的有效性
- 资损防控:建立资损实时校验机制,实时监控订单数量与库存扣减数量,出现不一致立即触发告警,必要时暂停活动
- 活动结束后数据归档:秒杀活动结束后,及时归档订单数据,清理缓存数据,释放系统资源,完成活动复盘与优化
总结
秒杀系统的架构设计,核心不是追求极致的技术炫技,而是基于业务场景,在性能、一致性、可用性之间找到最优平衡。从前端到数据层的全链路流量过滤,是秒杀系统扛住高并发的核心;Redis原子操作+消息队列异步化,是解决超卖、削峰填谷的核心方案;全链路的限流、熔断、降级,是系统高可用的核心保障。