秒杀系统高并发核心优化与落地全指南

0 阅读25分钟

一、秒杀系统的核心痛点与架构设计原则

秒杀是电商场景中最考验架构能力的业务场景之一,其本质是短时高并发冲击下,有限库存资源与海量用户请求的强冲突,同时对数据一致性、系统可用性有极致要求。

核心业务痛点

  1. 瞬时流量洪峰:秒杀开启瞬间,QPS可能从日常几百飙升至数十万甚至百万级,常规架构会被直接打穿
  2. 库存超卖风险:多线程并发下,库存的「读-改-写」非原子操作,极易出现库存为负的资损问题
  3. 读多写少特征:99%以上的请求是活动、库存查询,真正能完成下单的请求仅与库存数量同级
  4. 恶意请求冲击:黄牛脚本、刷量工具会发起大量无效请求,挤占正常用户的系统资源
  5. 链路雪崩风险:单环节故障会沿调用链向上传导,最终导致全系统宕机

核心架构设计原则

  1. 漏斗型流量过滤:将无效请求在链路最前端拦截,越往底层数据层,请求量越少,最大程度保护核心存储
  2. 读写强分离:读请求全链路走缓存,写请求全链路异步化,避免读写资源竞争
  3. 一致性优先:宁可少卖、绝不超卖,库存操作必须保证原子性,资损是秒杀系统的最高级故障
  4. 异步解耦削峰:非核心流程全量异步化,用消息队列将瞬时洪峰平摊到更长时间窗口处理
  5. 全链路兜底:每一层都必须有限流、熔断、降级方案,极端场景下保证系统不宕机、核心功能可用

二、秒杀系统全链路架构设计

全链路架构遵循「流量逐层收敛」的核心逻辑,从用户端到数据库,每一层都承担对应的流量过滤、请求处理能力,最终只有极少量有效请求能到达数据库层。

三、全链路分层核心优化方案

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行级锁,锁住库存记录,同一时间只有一个线程能读取和扣减库存,完全避免超卖。

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版本号字段,每次更新时校验版本号,只有版本号匹配才能更新成功,保证同一时间只有一个线程能扣减成功。

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脚本(原子预扣减):

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脚本时,不会被其他命令打断,完全保证操作的原子性,绝对不会出现超卖

  • 全流程逻辑:

    1. 秒杀前将库存预热到Redis
    2. 用户下单时,执行Lua脚本预扣减库存,扣减失败直接返回库存不足
    3. 预扣减成功,发送下单消息到消息队列,返回用户排队中
    4. 消费端异步消费消息,扣减数据库库存,创建订单
    5. 订单创建失败,回补Redis库存,保证数据最终一致性
  • 优势:并发能力极高,Redis单节点可扛10万+QPS,绝大部分请求在缓存层拦截,数据库压力极小,完全杜绝超卖

  • 适用场景:高并发秒杀的生产级核心方案

2. 分布式限流算法对比与选型

限流是秒杀系统保护自身的核心手段,核心目标是将请求量控制在系统能承载的范围内,避免系统被洪峰打垮。

算法类型核心原理优势劣势适用场景
固定窗口限流将时间划分为固定窗口,每个窗口内设置最大请求数,超过则限流实现简单,占用资源少存在临界问题,两个窗口交界处可能出现双倍流量冲击简单的粗粒度限流场景
滑动窗口限流将固定窗口划分为多个小格子,每次计算当前时间往前一个窗口内的总请求数,超过则限流解决了固定窗口的临界问题,限流精度高实现相对复杂,占用资源更多网关层的精准全局限流
漏桶算法请求进入漏桶,漏桶以固定速率流出请求,超过桶容量的请求直接被拒绝严格控制请求速率,流量绝对平滑无法应对突发流量,桶满时正常请求也会被拒绝接入层的IP级限流
令牌桶算法系统以固定速率往桶里放令牌,请求需获取令牌才能处理,桶有最大容量既能平滑限流,又能应对突发流量,灵活性高实现相对复杂业务层的接口级限流

生产级秒杀系统中,网关层采用滑动窗口实现全局限流,业务层采用令牌桶实现接口级限流,接入层采用漏桶实现IP级限流,形成三层限流防护体系。

3. 数据一致性保障

秒杀系统的一致性核心是库存数据的一致性,即Redis预扣库存与数据库实际库存的一致性,订单创建与库存扣减的一致性,采用「最终一致性」模型,优先保证不超卖,再保证数据最终一致。

核心保障方案:

  1. 消息可靠消费:采用RocketMQ的事务消息,保证Redis预扣减与消息发送的原子性,要么都成功,要么都失败,避免预扣减成功但消息未发送导致的库存冻结
  2. 库存回补机制:消费端创建订单失败时,必须原子回补Redis库存,同时清除用户的下单标记,避免库存永久冻结
  3. 幂等性全链路覆盖:所有请求、消息都基于唯一请求ID做幂等校验,避免重复请求、重复消费导致的重复扣减库存
  4. 定时对账校准:每日凌晨执行定时对账任务,对比Redis库存与数据库库存,以数据库实际库存为准,修正Redis库存,解决长期运行中的数据不一致问题
  5. 兜底事务控制:数据库的库存扣减与订单创建放在同一个本地事务中,要么都成功,要么都回滚,保证数据库层面的强一致性

4. 高可用兜底方案

秒杀系统必须做到「极端场景下不宕机,核心功能可用」,全链路每一层都必须有兜底方案。

  1. 服务熔断降级:基于Resilience4j实现服务熔断,当服务异常率、响应超时率超过阈值,自动触发熔断,直接返回友好提示,避免级联故障;系统压力过大时,关闭非核心功能,只保留秒杀下单、订单查询核心接口,释放系统资源
  2. 集群弹性扩容:提前准备好弹性扩容方案,基于监控数据,当QPS、CPU使用率超过阈值时,自动扩容业务集群、网关集群、Redis分片,线性提升系统处理能力
  3. 多级兜底限流:前端、Nginx、网关、业务层、Redis、数据库每一层都设置最大QPS阈值,超过阈值直接拒绝请求,保证每一层都不会被打垮,形成全链路防护
  4. 缓存降级兜底:当Redis集群出现故障,直接切换到数据库悲观锁方案,虽然并发能力下降,但保证秒杀核心功能可用,不会完全瘫痪
  5. 全链路监控告警:对全链路的QPS、响应时间、异常率、库存数量、消息堆积量等核心指标做实时监控,设置多级告警阈值,出现异常立即通知运维人员介入,提前规避故障

五、代码实现

1. 数据库表结构(MySQL 8.0)

CREATE TABLE `seckill_activity` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
  `activity_name` varchar(128NOT 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(128NOT NULL COMMENT '商品名称',
  `original_price` decimal(10,2NOT NULL COMMENT '原价',
  `seckill_price` decimal(10,2NOT 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(64NOT 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,2NOT NULL COMMENT '订单金额',
  `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已取消,3-已完成',
  `request_id` varchar(64NOT 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 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)

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. 核心实体类

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;
}
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;
}
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)

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> {
}
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);
}
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. 业务服务层核心实现

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);
    }
}
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. 消息消费者实现

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. 接口控制器实现

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. 项目启动类

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. 线上运维核心规范

  1. 提前预热:秒杀开启前2小时完成数据预热,提前扩容集群节点,开启全链路监控
  2. 灰度发布:秒杀相关的系统变更,必须提前3天完成灰度发布,验证无问题后全量上线
  3. 全链路压测:每次大促秒杀前,必须完成全链路压测,验证系统的最大承载能力,预留30%以上的冗余容量
  4. 实时监控告警:对QPS、响应时间、异常率、库存数量、消息堆积量、CPU、内存、磁盘IO等核心指标做实时监控,设置多级告警阈值,出现异常立即通知相关人员
  5. 容灾演练:定期进行容灾演练,模拟Redis宕机、消息队列宕机、数据库宕机等极端场景,验证兜底方案的有效性
  6. 资损防控:建立资损实时校验机制,实时监控订单数量与库存扣减数量,出现不一致立即触发告警,必要时暂停活动
  7. 活动结束后数据归档:秒杀活动结束后,及时归档订单数据,清理缓存数据,释放系统资源,完成活动复盘与优化

总结

秒杀系统的架构设计,核心不是追求极致的技术炫技,而是基于业务场景,在性能、一致性、可用性之间找到最优平衡。从前端到数据层的全链路流量过滤,是秒杀系统扛住高并发的核心;Redis原子操作+消息队列异步化,是解决超卖、削峰填谷的核心方案;全链路的限流、熔断、降级,是系统高可用的核心保障。