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

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

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

核心业务痛点

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

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

  • 全流程逻辑:

    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)

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

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

总结

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

相关推荐
野犬寒鸦2 小时前
JVM垃圾回收机制面试常问问题及详解
java·服务器·开发语言·jvm·后端·算法·面试
风酥糖2 小时前
Godot游戏练习01-第16节-游戏中的状态机
算法·游戏·godot
budingxiaomoli2 小时前
优选算法--优先级队列(堆)
算法
Trouvaille ~2 小时前
【优选算法篇】哈希表——空间换时间的极致艺术
c++·算法·leetcode·青少年编程·蓝桥杯·哈希算法·散列表
bbbb3652 小时前
算法调优的多目标优化与性能平衡模型的技术8
算法
Fcy6482 小时前
与二叉树有关算法题
算法·深度优先
const_qiu2 小时前
微服务测试项目架构设计与实践
微服务·云原生·架构
️是782 小时前
信息奥赛一本通—编程启蒙(3346:【例60.3】 找素数)
数据结构·c++·算法
captain3762 小时前
map和set
数据结构·算法