# 秒杀系统 **Repository Path**: hnu-turing/seckill-system ## Basic Information - **Project Name**: 秒杀系统 - **Description**: 基于 Spring Boot 的高并发秒杀系统示例,包含 **商品 / 订单 / 支付** 三大模块, 覆盖防超卖、削峰、缓存、限流、索引优化、乐观锁、Redis 熔断、异步 ES 同步、 Redis-MySQL 一致性 等生产级治理手段。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 9 - **Forks**: 0 - **Created**: 2026-08-10 - **Last Updated**: 2026-08-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 秒杀系统(seckill-system) 基于 Spring Boot 的高并发秒杀系统示例,包含 **商品 / 订单 / 支付** 三大模块, 覆盖防超卖、削峰、缓存、限流、索引优化、乐观锁、Redis 熔断、异步 ES 同步、 Redis-MySQL 一致性 等生产级治理手段。 ## 快速启动 ```powershell # 1. 启动 Docker Desktop(开始菜单点开,等右下角鲸鱼图标稳定) # 2. 一键起中间件:MySQL / Redis / RabbitMQ / ES(首次会拉镜像) docker compose up -d # 3. 启动应用(自动打包 + 补齐依赖 + 运行) .\start.ps1 ``` - 演示入口: http://localhost:8080/index.html (用户视角秒杀/下单/支付) - 管理后台: http://localhost:8080/dashboard.html (概览+对账)、products.html、activities.html、orders.html、balance.html - RabbitMQ 控制台: http://localhost:15672 (guest/guest) - 重置演示数据: `docker compose down -v && docker compose up -d`(MySQL 会重跑 sql/schema.sql) - 全部地址可用环境变量覆盖(MYSQL_HOST / REDIS_HOST / RABBIT_HOST / ES_URIS),见 `application.yml` ## 技术栈 | 组件 | 选型 | 用途 | |---|---|---| | Spring Boot 2.7 + MyBatis-Plus 3.5 | 应用框架 | `@Version` 乐观锁、分页 | | Redis + Redisson 3.17 | 缓存 / 预扣库存 / 分布式锁 / 令牌桶限流 | `RRateLimiter` | | RabbitMQ | 削峰异步下单、延迟队列、异步 ES 同步、缓存延时双删 | TTL + DLX | | MySQL 8 | 持久层 | 联合索引、唯一索引、库存/余额条件更新 | | Elasticsearch 7.12 | 商品搜索 | MQ 异步同步,最终一致 | | 自研轻量熔断器 | Redis 熔断降级 | CLOSED/OPEN/HALF_OPEN,可无缝替换 Resilience4j | ## 核心设计 ### 1. 防超卖(四道防线) ``` 用户请求 → Redisson令牌桶限流 → 活动校验(缓存) → ① Redis Lua 原子预扣(去重+扣减) → ② 发 MQ 异步落库(削峰) → ③ MySQL 条件更新 stock >= n(CAS 兜底) → ④ uk_user_activity 唯一索引兜底防重复 ``` - ① `lua/seckill_deduct.lua`:查重 + 校验库存 + 扣减一条 Lua 原子完成,杜绝并发脏读 - ② 请求只写 Redis + 发 MQ 即返回"排队中",DB 写压力被 MQ 削平 - ③ `SeckillActivityMapper.deductStock`:`UPDATE ... WHERE stock >= #{n} AND status = 1`, 影响行数 = 0 即失败,天然不会扣成负数 - ④ `seckill_order.uk_user_activity(user_id, activity_id)` 唯一索引,一人一单 DB 硬约束 - 订单创建失败 / 超时关闭 / 用户取消 → `lua/seckill_revert.lua` 原子回补 Redis 库存 + MySQL `addStock` ### 2. 削峰:异步下单 + 延迟关单 - `seckill.exchange/order.create` → `OrderCreateConsumer` 批量建单(消费速度≠洪峰速度) - 订单创建后向 **TTL 延迟队列** 投递超时检测消息,30min 到期经死信交换机(DLX) 路由到 `order.timeout`,状态机 CAS `待支付→超时关闭` 并回补库存 - 消息幂等:`orderNo` 查重 + 状态机 CAS,重复消费/重复投递安全 ### 3. 缓存体系(读多写少全链路) | 风险 | 方案 | |---|---| | 穿透 | 不存在的 id 缓存 `NULL` 标记,短 TTL | | 击穿 | Redisson 分布式锁串行重建 + 锁内双重检查;抢不到锁直接查 DB 不堆积 | | 雪崩 | 基础 TTL + 随机抖动(600s 窗口)分散过期时间 | | 一致性 | 更新 DB → 同步删缓存 → **MQ 延时 1s 二次删除**(兜住删除窗内的并发读回填) | ### 4. 限流:Redisson 令牌桶 - `RRateLimiter` 基于 Redis Lua 实现令牌生成/消费原子化,集群维度精确限流 - `@RateLimit` 支持 SpEL 模板 key:`{key={#request.activityId}}`,可叠加多条规则 - 秒杀接口默认:全局 8000 QPS + 每活动 1000 QPS,超过直接返回 429 - **Redis 故障时降级为 JVM 本地令牌桶**(单机维度),限流能力不丢失 ### 5. Redis 熔断降级 `RedisCircuitBreaker`(`breaker/RedisCircuitBreaker.java`): - 滑动窗口(默认 20 次调用)内基础设施异常比例 ≥ 60% → **熔断 OPEN** - OPEN 期间 `RedisExecutor` 统一降级:秒杀快速失败(保护线程池)、 商品读降级直查 DB、缓存写跳过(延时双删兜底) - 5s 半开探测,连续 3 次成功自动恢复 CLOSED - 只统计连接失败/超时等基础设施异常,业务异常不计入 ### 6. MySQL 索引与乐观锁 - 按访问路径建联合索引:`uk_order_no`、`uk_user_activity`、`idx_status_expire`(超时单扫描)、`idx_status_time` - 实体 `@Version` + MyBatis-Plus 乐观锁拦截器:更新自动 `version = version + 1` 且 `WHERE version = 旧值` - 状态流转统一 CAS(`casStatus`),并发回调/关单幂等 ### 7. Redis/MySQL 最终一致性(对账闭环) **收敛链路的每一步都设计成天然一致**,再由定时对账任务兜底漂移: - 写路径:秒杀预扣 Redis → MQ 建单扣 DB;取消/超时/退款 → Lua 回补 Redis + DB `addStock` 双端回补 - 失败路径:MQ 投递失败立即回补 Redis;消费端确定性失败回补 Redis;Redis 故障期走熔断降级 - 对账任务 `ReconcileService`(每 60s,启动 15s 后首轮,可手动 `POST /api/reconcile/run`): 1. **库存对账**:进行中的活动,比对 `seckill:stock:{id}` 与 DB 库存,漂移/丢键 → 以 DB 为唯一账本 `SET` 修正;Redis 熔断期自动跳过本轮 2. **活动缓存对账**:比对缓存与 DB 的读路径字段(价格/状态/时间/限购),漂移 → 删缓存由下次读重建 3. **滞留订单对账**:扫描 已过期仍待支付 的订单(延迟消息丢失兜底)→ 状态机 CAS 幂等关单 + 库存回补 - 报告落内存并暴露在管理后台「数据概览 → 最终一致性对账」面板 ### 8. 异步 ES 同步 商品写路径只发消息,`EsSyncConsumer` 消费时**从 DB 取最新值写 ES**(索引 `seckill_product`): 主链路零 ES 依赖,ES 宕机仅告警;最终一致可配定时全量同步任务对齐。 ## 压测脚本 `bench/SeckillBench.java`(纯 JDK,无第三方依赖): ```powershell javac -encoding UTF-8 -d bench\out bench\SeckillBench.java java -cp bench\out SeckillBench --activity 1 --users 3000 --concurrency 200 ``` - 三阶段:突发下单 → 等 MQ 消费完轮询库存 → 触发对账并打印报告 - 验证点:QUEUED 数 ≤ 剩余库存(防超卖)、超桶请求返回 429(限流)、 队列立即返回 QUEUED(削峰)、对账 fixedCount=0(Redis/MySQL 一致) - 实测:3000 并发抢 50 件库存,恰好 50 单 QUEUED、2950 单售罄拒绝, 落库 50 笔(version=50 次 CAS),对账零修复 ## 管理后台与演示页 应用启动后直接浏览器访问(静态页面随 jar 内置,无需额外部署): | 页面 | 地址 | 功能 | |---|---|---| | 数据概览 | `/dashboard.html` | 今日订单/待支付/GMV/累计销量/进行中活动统计卡,最新订单流 | | 秒杀演示 | `/` | 多活动切换、倒计时、Redis 实时库存(2s 轮询)、一键秒杀、排队进度轮询 | | 商品管理 | `/products.html` | 商品 CRUD:分页/名称搜索、新增编辑弹窗、上下架、删除(双删缓存+ES同步) | | 库存管理 | `/activities.html` | 秒杀活动 CRUD:创建/编辑/上下线,DB 与 Redis 库存并列展示、库存变更实时同步 Redis | | 订单管理 | `/orders.html` | 按用户/状态筛选分页,支付、取消(回补库存)、退款(回补余额+库存)、删除(仅关闭态) | | 余额账户 | `/balance.html` | 余额卡片、快捷充值弹窗、支付流水(8s 自动刷新) | 后台风格:左侧导航 + 顶部用户切换(1001~1005),纯原生 HTML/JS(离线零依赖)。 演示流程:数据概览 → 秒杀演示下单(观察实时库存下降)→ 订单管理支付 → 余额账户看流水 → 取消/退款观察库存与余额回补。 配套后端:用户余额账户(`user_account` 表 + CAS 扣款,不产生负余额; 订单超时关闭/支付失败/退款自动回补);订单状态机扩展了 `4已退款`。 ## 快速开始 ```bash # 1. 一键启动中间件(MySQL 初始化脚本自动执行) docker compose up -d # 2. 构建(联网环境直接 mvn package;无网环境见下方"离线构建") mvn clean package # 3. 启动 java -jar target/seckill-system.jar # 或者:mvn org.springframework.boot:spring-boot-maven-plugin:2.5.15:run ``` 默认端口 8080,演示数据:`product#1 / activity#1`(库存 50,活动 2 天内有效)。 ### 接口速览 ```bash # 秒杀(userId + activityId)→ 返回排队中的 orderNo curl -X POST http://localhost:8080/api/seckill \ -H "Content-Type: application/json" \ -d '{"userId":1001,"activityId":1,"quantity":1}' # 轮询订单状态 / 取消 curl "http://localhost:8080/api/orders/{orderNo}?userId=1001" curl -X POST "http://localhost:8080/api/orders/{orderNo}/cancel?userId=1001" # 收银台:创建支付单 → 模拟回调 curl -X POST "http://localhost:8080/api/payments?orderNo={orderNo}&userId=1001" curl -X POST "http://localhost:8080/api/payments/{payNo}/mock-pay" # 余额:查询 / 充值(前端演示用) curl http://localhost:8080/api/accounts/1001 curl -X POST "http://localhost:8080/api/accounts/1001/recharge?amount=1000" # 商品:缓存读 / ES 搜索 / 后台写 curl http://localhost:8080/api/products/1 curl "http://localhost:8080/api/products/search?keyword=iPhone" curl -X PUT http://localhost:8080/api/products/1 -H "Content-Type: application/json" -d '{...}' ``` ### 压测脚本示例(验证限流/防超卖) ```bash # 200 并发 * 10 用户,观察:订单数 ≤ 库存数,无重复订单,超过 QPS 返回 429 for /l %i in (1,1,50) do curl -s -X POST http://localhost:8080/api/seckill -H "Content-Type: application/json" -d "{\"userId\":%i,\"activityId\":1,\"quantity\":1}" SELECT COUNT(*), SUM(quantity) FROM seckill_order; -- ≤ 活动库存 SELECT stock FROM seckill_activity WHERE id=1; -- 与 Redis seckill:stock:1 一致 ``` ## 离线构建(本机内置仓库缓存环境) 本地 `~/.m2` 已缓存全部依赖但无外网时: ```bash mvn -o -s settings-offline.xml clean package -DskipTests ``` - `settings-offline.xml` 声明历史下载来源的仓库 id,使 Maven 认可本地缓存 - 离线仓库缺少 fat-jar 所需的 `spring-boot-loader`,产物为 **薄 jar**,用 `spring-boot:run` 即可运行 - 离线仓库同样缺少 surefire 的 JUnit5 provider,核心逻辑单测通过 `src/test/java/com/seckill/RunTests.java`(极简反射运行器)执行: `mvn -o -s settings-offline.xml test-compile -DskipTests` 后 `java -cp ... com.seckill.RunTests` ## 目录结构 ``` src/main/java/com/seckill ├── breaker # Redis 熔断器(三态切换 + 滑动窗口失败率统计) ├── cache # RedisExecutor(熔断包装) / ProductCacheService(防穿透/击穿/雪崩) / RedisKeys ├── config # MQ 拓扑(交换机/队列/TTL+DLX) / MyBatis-Plus(乐观锁) / Lua 脚本 Bean / 库存预热 ├── controller # 商品 / 秒杀 / 订单 / 支付 REST 接口 ├── dto # 请求校验 ├── entity # Product / SeckillActivity / SeckillOrder / SeckillPayment(@Version 乐观锁) ├── es # ProductSearchService(RestHighLevelClient 直写)/ ProductDoc ├── mapper # BaseMapper + 库存 CAS / 状态机 CAS 等自定义 SQL ├── mq # 生产者 / 4 个消费者(手动 ack + 幂等 + 确定性失败不重试) ├── ratelimit # @RateLimit + Redisson 令牌桶切面(含本地降级桶) └── service # Seckill / Order / Payment / Product 核心链路 src/main/resources/lua # 预扣 / 回补 原子脚本 sql/schema.sql # 建库建表(索引规范 + 演示数据) ``` ## 可扩展点(生产化建议) - 秒杀资格:接口地址动态化/验证码/答题/用户侧风控 - 库存对账:定时任务比对 Redis 与 MySQL 库存并告警 - 延迟队列:30min 全队列单 TTL;精细化粒度可换 RocketMQ 延迟消息或 Redisson DelayedQueue - 熔断器:替换 Resilience4j 原语(接口语义已对齐) - 缓存一致性更强:接 Canal 订阅 binlog 异步刷新缓存