秒杀系统真正难的,从来不是“把库存减掉”,而是在瞬时洪峰、热点商品、恶意流量、链路抖动和多组件故障同时出现时,依然做到不超卖、不拖垮主站、可快速扩容、可回溯复盘。
摘要
很多团队把秒杀当成“下单接口性能优化”问题,结果上线后发现真正被考验的并不只是 SQL 或 Redis,而是整套系统的流量治理能力、状态建模能力、异步解耦能力、故障隔离能力和运营保障能力。
一套生产级秒杀系统,本质上是在做四件事:
- 尽可能早地淘汰无效请求。
- 尽可能少地让请求进入数据库。
- 把同步强依赖改造成可控的异步状态机。
- 在极端 高并发 下,优先保证正确性和核心链路可用性。
本文从真实业务场景出发,系统讲清楚秒杀系统的原理、架构、工程化实现与生产治理,重点覆盖以下内容:
- 秒杀系统为什么不能只靠数据库扣库存
- 为什么“抢购成功”通常只是拿到资格,而不是立即落单
- Redis Lua、MQ 削峰、幂等、补偿、对账各自解决什么问题
- 如何支撑热点 SKU、高并发、恶意刷流量和多组件故障
- 如何做可上线的代码设计、容量评估、压测、观测与值班预案
如果你希望把一篇“能讲概念”的秒杀文章升级成“架构师能拿去做设计评审”的生产级稿件,这篇文章就是按这个目标写的。
一、为什么秒杀系统值得单独设计
秒杀不是普通下单的流量放大版,而是一个结构性不同的问题。
它同时具备以下几个特征:
- 流量极端不均匀:平时几百 QPS,开场瞬间可能冲到数万到数十万 QPS。
- 热点高度集中:90% 的请求可能打在 1 到 3 个热门 SKU 上。
- 用户容错极低:抢不到可以接受,但超卖、重复下单、付款后无单不可接受。
- 活动窗口极短:故障恢复时间以秒计算,而不是小时。
- 黑灰产密集:脚本刷号、代理池、打码平台、设备农场都会出现。
- 链路影响面大:活动流量一旦失控,常常拖垮商品、购物车、支付、订单查询等主站链路。
所以,秒杀系统不能只追求“吞吐高”,而要同时满足四个目标:
- 正确性优先:库存不能超卖,用户维度不能重复下单。
- 系统隔离:秒杀链路不能拖垮交易主站。
- 无效请求前置淘汰:越早拦截越好。
- 工程可运营:能预热、能扩容、能降级、能对账、能复盘。
换句话说,秒杀系统考验的不是某个中间件用得熟不熟,而是你能不能把一个极端热点问题改造成一套可控的分层协同系统。
二、目标场景与非功能性约束
为了避免方案只停留在概念层,先定义一个更接近真实业务的活动场景。
2.1 业务场景
某电商平台在大促期间上线一场秒杀活动:
- 单场活动 SKU 数量:20 个
- 头部热点 SKU:3 个
- 预约用户:300 万
- 活动开场前 10 秒在线用户:80 万
- 峰值页面读流量:300,000 QPS
- 峰值有效抢购请求:60,000 QPS
- 单 SKU 库存:2,000 到 50,000 不等
- 下单结果反馈目标:主提交通道 P99 小于 80ms
2.2 非功能性目标
| 维度 |
目标 |
| 正确性 |
库存不超卖,用户维度严格幂等 |
| 性能 |
抢购接口快速返回,P99 小于 80ms |
| 可用性 |
核心链路活动期间可用性 99.99% |
| 扩展性 |
Web、Consumer、风控服务可水平扩容 |
| 隔离性 |
秒杀流量与普通交易流量资源隔离 |
| 可恢复性 |
Redis、MQ、DB 异常时具备降级和补偿能力 |
| 可观测性 |
支持日志、指标、链路追踪、审计与库存对账 |
2.3 设计边界
在真正做方案前,还要先讲清楚边界,否则很容易在评审会上陷入“为什么不做得更强一致”的空谈。
这套方案默认接受以下边界:
- 用户同步拿到的通常是“排队资格”,而不是最终订单号。
- 秒杀结果默认接受最终一致性,不追求 Redis、MQ、MySQL 全链路同步强事务。
- 对极端场景,宁可少卖,不可超卖。
- 风控误杀和限流误伤可以通过运营补偿解决,但超卖和主站雪崩很难挽回。
这是生产系统中非常务实的一组取舍。
三、秒杀系统的本质:把“同步抢库存”改造成“分层过滤 + 异步确认”
秒杀系统的核心思想可以概括为 16 个字:
前置过滤,缓存承压,消息削峰,异步落单。
很多新手方案的思路是:
用户请求 -> 应用服务 -> 数据库扣库存 -> 创建订单 -> 返回结果
这个流程在普通下单场景没问题,但在秒杀场景会迅速暴露出三个问题:
- 数据库成为并发热点,连接池和行锁会被瞬间打满。
- 无效请求和有效请求混在一起,浪费核心资源。
- 同步链路太长,任何一个依赖抖动都会放大成整体雪崩。
生产级秒杀系统会改造成:
用户请求
-> 入口限流/风控/资格校验
-> Redis 原子占位
-> MQ 排队削峰
-> Consumer 异步落单
-> 前端轮询或推送结果
这背后的结构变化有两个关键点。
3.1 从“同步事务思维”切换到“状态机思维”
秒杀不是一次单纯的同步调用,而是一条跨组件状态流转链路。一次请求通常会经历下面几个状态:
INIT
-> TOKEN_VERIFIED
-> RISK_PASSED
-> STOCK_OCCUPIED
-> MQ_QUEUED
-> ORDER_CREATED
-> SUCCESS
如果某个阶段失败,还会进入:
REJECTED
FAILED
COMPENSATING
ROLLED_BACK
也就是说,秒杀系统真正要设计的不是一个接口,而是一套可追踪、可补偿、可审计的状态机。
3.2 从“所有请求都进核心链路”切换到“分层淘汰”
成熟系统通常只允许极少数有效请求进入最重的链路。
分层淘汰顺序通常如下:
- 入口层淘汰:未登录、签名错误、无资格、明显异常流量直接拒绝。
- 缓存层淘汰:活动未开始、库存售罄、重复抢购在 Redis 阶段快速结束。
- 消息层削峰:拿到资格的请求先排队,不立即写库。
- 消费层落单:异步串起订单创建、结果回写和补偿。
- 查询层回查:前端通过轮询、SSE 或 WebSocket 获取最终状态。
这才是高并发秒杀真正能“抢不崩”的底层逻辑。
四、架构演进:从单机到生产级分布式
秒杀系统的演进通常不是一步到位,而是随着业务体量逐步升级。
4.1 阶段一:单机版
Client -> Spring Boot -> Redis -> MySQL
这个阶段常用于验证业务模式,优点是简单、开发快,但问题也最明显:
- 库存判断与用户防重逻辑容易分散在多个方法中
- MySQL 很快成为写瓶颈
- 扩容后本地状态难统一
- 没有削峰和隔离,系统很容易被瞬时洪峰打穿
4.2 阶段二:缓存前置 + 本地热点缓存
Client -> Nginx -> Web Cluster -> Caffeine + Redis Cluster -> MySQL
这个阶段解决的是读流量问题:
- 商品详情和活动配置前移到缓存
- 入口增加基础限流
- 数据库从商品读流量中解放出来
但它仍然无法解决写流量冲击数据库的问题。
4.3 阶段三:生产级核心形态
CDN/WAF
-> Nginx/OpenResty
-> Gateway
-> Seckill Service
-> Redis Cluster
-> MQ
-> Order Consumer Cluster
-> MySQL / 分库分表
这个阶段的关键升级是:
- 库存竞争从数据库前移到 Redis Lua
- 同步落单改为异步消息消费
- 抢购结果从“同步返回订单”改为“返回排队票据 + 异步查询”
4.4 阶段四:单元化与多地域容灾
Global DNS / GSLB
-> Region Unit A/B/C
-> Unit-local Gateway + Redis + MQ + DB
当业务已经有全国化部署、跨地域机房或者超大活动规模时,通常会进一步单元化:
- 用户按地域、渠道、用户分群打到固定单元
- 单元内部闭环处理库存、下单、支付编排
- 跨单元尽量减少强依赖,降低跨地域延迟和故障传播
这类架构的目标不只是“更快”,而是“更隔离、更稳、更容易控制故障爆炸半径”。
五、生产级架构全景图
下面给出一套更完整的生产级秒杀架构全景。
┌─────────────────────────────┐
│ Client │
└──────────────┬──────────────┘
│
静态页/JS/CDN 缓存
│
┌──────────────▼──────────────┐
│ WAF + Anti-Bot │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Nginx/OpenResty Gateway │
│ 限流、签名校验、灰度、熔断 │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Seckill Service │
│ 资格校验、令牌校验、排队入 MQ │
└───────┬──────────┬──────────┘
│ │
┌─────────────▼───┐ ┌──▼────────────────┐
│ Redis Cluster │ │ Risk Service │
│ 库存、幂等、令牌 │ │ 设备/IP/账号风控 │
└─────────────┬───┘ └───────────────────┘
│
┌───────▼───────────────────────┐
│ RocketMQ / Kafka │
│ 削峰、解耦、重试 │
└───────┬───────────────────────┘
│
┌───────▼───────────────────────┐
│ Order Consumer Cluster │
│ 幂等、事务、订单落库、补偿 │
└───────┬───────────────────────┘
│
┌──────────────▼───────────────┐
│ MySQL / ShardingSphere / DB │
│ 订单表、流水表、库存快照、对账表 │
└──────────────────────────────┘
5.1 各层的职责分工
| 层 |
核心职责 |
| CDN/静态页 |
承接大部分商品页和倒计时读流量 |
| WAF/Anti-Bot |
黑名单、签名、设备指纹、异常流量拦截 |
| OpenResty/Gateway |
限流、路由、灰度、快速失败 |
| Seckill Service |
资格校验、令牌验证、库存占位、消息投递 |
| Redis |
热点承压、原子占位、请求防重、结果缓存 |
| MQ |
削峰填谷、异步化、解耦核心依赖 |
| Consumer |
幂等落单、事务处理、结果回写、补偿 |
| MySQL |
最终订单事实、流水审计、库存快照与对账依据 |
5.2 这一架构真正解决了什么
- 绝大多数无效请求不会进入 DB
- 有效请求不会同步打爆订单服务
- 抢购接口可以快速返回
- 风控、库存、订单、查询各层职责明确
- 出问题时可以按层降级,而不是整站一起崩
六、核心设计原则
6.1 不在数据库层抢库存
如果抢购时直接执行如下 SQL:
UPDATE sku_stock
SET available_stock = available_stock - 1
WHERE sku_id = ? AND available_stock > 0;
它在语义上可以避免明显超卖,但在高并发下有三个根本问题:
- 所有请求仍然会冲到数据库
- 热点行会形成严重锁竞争
- 连接池和事务开销会快速放大 RT
所以生产级方案的重点不是“数据库能不能扣成功”,而是“数据库应不应该承受这波流量”。答案通常是不应该。
6.2 抢购资格与订单创建解耦
用户点击抢购时,系统先做的是“争抢资格”,而不是“同步创建订单”。
也就是说:
- Redis 负责判定是否还有资格
- MQ 负责排队
- Consumer 负责最终落单
这能显著缩短同步链路长度,把数据库从洪峰前线撤下来。
6.3 重复请求必须在最前面拦截
用户会重复点击,脚本会高频重放,客户端会超时重试。
如果幂等只依赖数据库唯一键,数据库虽然最终能兜底,但已经被重复请求拖死了。因此幂等至少要做四层:
- 网关层限流
- Redis 用户抢购标记
- MQ 消费幂等
- 数据库唯一索引兜底
6.4 秒杀系统默认接受最终一致性
Redis、MQ、MySQL 三者之间做跨组件强事务,理论上可以更“严谨”,但工程上通常非常昂贵,而且容易把高可用做没。
多数成熟系统采取的是:
- Redis 先占位
- MQ 异步下单
- DB 落单成功后回写结果
- 异常时通过补偿回滚占位或修正状态
这是以系统可用性和吞吐优先的最终一致性架构。
6.5 宁可少卖,不可超卖
在 Redis 抖动、MQ 不稳定、DB 瞬时故障等场景下,秒杀系统的默认策略应该是保守:
- 超时直接拒绝或返回排队中
- 不冒险“猜测成功”
- 以用户补偿代替系统失控
因为少卖可以运营补偿,超卖往往是品牌事故。
七、一次秒杀请求究竟经历了什么
7.1 活动开始前:预热阶段
秒杀系统稳定性的很大一部分,来自活动前准备而不是活动开始后的在线处理。
预热阶段通常要完成以下动作:
- 将活动信息、SKU、库存、限购规则加载到 Redis。
- 将静态页、商品详情、倒计时资源推送到 CDN。
- 生成预约资格或抢购令牌。
- 扩容 Gateway、Seckill Service、Consumer。
- 验证风控、Redis、MQ、DB 的容量与告警阈值。
- 准备降级开关、只读页和人工预案。
7.2 用户点击抢购:同步快速返回阶段
同步链路应尽量短,只做高价值判断:
- 校验登录态和活动状态
- 校验资格令牌和签名
- 做风控判断和频控判断
- 用 Redis Lua 原子占位
- 生成请求号并投递 MQ
- 返回排队中结果
注意,这一阶段的目标不是“创建订单”,而是“尽快且准确地决定该请求是否值得进入队列”。
7.3 后台异步消费:真实落单阶段
Consumer 承担真正的订单创建工作:
- 按消息唯一 ID 做消费幂等
- 校验 DB 中是否已有该用户该活动该 SKU 的订单
- 写订单、明细、审计流水
- 更新请求状态和结果缓存
- 必要时触发支付链路或通知链路
7.4 用户查询结果:弱实时回查阶段
用户侧不应依赖同步返回订单号,而是通过下面几种方式拿结果:
- 轮询
/result
- WebSocket 或 SSE 推送
- 我的订单异步出现
这可以显著缩短主链路 RT,降低同步耦合。
7.5 异常场景:补偿阶段
如果出现以下问题:
- MQ 投递失败
- Consumer 落单失败
- DB 插入异常
- 请求状态未及时回写
系统应进入补偿流程:
- 标记请求为失败或补偿中
- 回滚 Redis 占位
- 记录审计日志
- 通过定时任务或人工脚本重试修复
这也是为什么秒杀系统一定要有请求流水表,而不是只看最终订单表。
八、生产级数据模型设计
秒杀系统的数据模型不能只围绕订单表设计,还必须覆盖“请求事实”“库存事实”和“补偿事实”。
8.1 订单表
CREATE TABLE `seckill_order` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`activity_id` BIGINT NOT NULL,
`sku_id` BIGINT NOT NULL,
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消 3-已关闭',
`source` VARCHAR(16) NOT NULL DEFAULT 'SECKILL',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
UNIQUE KEY `uk_activity_sku_user` (`activity_id`, `sku_id`, `user_id`),
KEY `idx_user_ctime` (`user_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里的 (activity_id, sku_id, user_id) 唯一索引是最终兜底,防止任何上游幂等失效后重复下单。
8.2 请求流水表
这张表用来记录一次用户抢购请求的完整生命周期。
CREATE TABLE `seckill_request_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`request_no` VARCHAR(32) NOT NULL,
`activity_id` BIGINT NOT NULL,
`sku_id` BIGINT NOT NULL,
`user_id` BIGINT NOT NULL,
`request_status` TINYINT NOT NULL COMMENT '0-排队中 1-成功 2-售罄 3-重复 4-风控拦截 5-系统失败 6-补偿中',
`message_id` VARCHAR(64) DEFAULT NULL,
`order_no` VARCHAR(32) DEFAULT NULL,
`trace_id` VARCHAR(64) DEFAULT NULL,
`remark` VARCHAR(128) DEFAULT NULL,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_request_no` (`request_no`),
KEY `idx_user_activity` (`user_id`, `activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
它解决的典型问题是:
- 用户说自己“已经抢到了但没有订单”
- 运营问某个用户为什么被拦截
- 技术排查消息是否成功投递和消费
8.3 库存快照表
CREATE TABLE `seckill_stock_snapshot` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`activity_id` BIGINT NOT NULL,
`sku_id` BIGINT NOT NULL,
`initial_stock` INT NOT NULL,
`available_stock` INT NOT NULL,
`sold_stock` INT NOT NULL DEFAULT 0,
`version` INT NOT NULL DEFAULT 0,
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_activity_sku` (`activity_id`, `sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意,DB 中的库存快照不是用来扛高并发写的,而是用来:
8.4 结果缓存设计
用户结果查询不能每次都查 DB,通常会在 Redis 中维护结果 Key:
seckill:result:{requestNo} -> QUEUING | SUCCESS:{orderNo} | FAIL:{code}
这样用户轮询时,大部分请求只读 Redis,不会打到订单库。
九、库存扣减的关键原理:为什么必须用 Lua
很多错误实现会这样做:
- 先判断库存是否大于 0
- 再做
DECR
- 再写用户抢购标记
问题在于这三步如果不在 Redis 端原子执行,就会出现:
- 多个线程同时看到库存大于 0
- 同一用户重复抢购
- 库存扣减成功但用户防重标记未落下
所以生产级库存占位逻辑必须收敛到 Lua 脚本中。
9.1 Lua 脚本应该承担哪些职责
一段生产可用的 Lua 脚本,通常要同时完成以下事情:
- 活动状态判断
- 用户重复抢购判断
- 库存是否充足判断
- 原子扣减库存
- 写入用户抢购标记
- 返回明确的业务状态码
9.2 生产级 Lua 示例
-- KEYS[1] = stock key
-- KEYS[2] = user set key
-- KEYS[3] = activity status key
-- ARGV[1] = userId
local activityStatus = redis.call('GET', KEYS[3])
if not activityStatus or activityStatus ~= 'ONLINE' then
return 0
end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return 2
end
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) <= 0 then
return 1
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
redis.call('EXPIRE', KEYS[2], 172800)
return 3
返回码建议统一约定:
0:活动未开始或已关闭
1:库存不足
2:重复抢购
3:成功占位
9.3 为什么 Lua 比简单 DECR 更重要
有人会说,直接 DECR 不是也能扣库存吗?
问题在于秒杀场景需要的不只是扣库存,而是同时满足:
- 库存不能超卖
- 同一用户不能重复抢
- 活动状态要可控
- 出错时要能识别失败原因
单独的 DECR 只能解决其中一小部分问题,而 Lua 是把多条业务约束在 Redis 端合成一次原子决策。
这就是它在秒杀系统里不可替代的原因。
十、生产级代码实现:从接口到异步落单
下面给出一套更接近生产环境的代码骨架。为了突出设计重点,省略部分样板代码,但链路和职责是完整可落地的。
10.1 请求对象与返回对象
public record SeckillRequest(
Long activityId,
Long skuId,
Long userId,
String token,
String clientIp,
String requestId) {
}
public record SeckillSubmitResult(
boolean accepted,
String requestNo,
String message) {
public static SeckillSubmitResult accepted(String requestNo) {
return new SeckillSubmitResult(true, requestNo, "排队中");
}
public static SeckillSubmitResult rejected(String message) {
return new SeckillSubmitResult(false, null, message);
}
}
10.2 Controller:接入层只做参数接收与身份透传
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
private final SeckillApplicationService seckillApplicationService;
public SeckillController(SeckillApplicationService seckillApplicationService) {
this.seckillApplicationService = seckillApplicationService;
}
@PostMapping("/submit")
public ResponseEntity<SeckillSubmitResult> submit(@Valid @RequestBody SeckillSubmitCommand command,
@RequestHeader("X-User-Id") Long userId,
HttpServletRequest request) {
SeckillRequest seckillRequest = new SeckillRequest(
command.activityId(),
command.skuId(),
userId,
command.token(),
request.getRemoteAddr(),
command.requestId()
);
SeckillSubmitResult result = seckillApplicationService.submit(seckillRequest);
return result.accepted()
? ResponseEntity.ok(result)
: ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
}
}
Controller 不应该承担复杂业务逻辑。它的职责是:
- 做参数接收与校验
- 获取用户信息和请求上下文
- 调用应用服务
- 统一返回规范结果
10.3 Application Service:资格校验、限流、占位、投递消息
@Service
public class SeckillApplicationService {
private final AccessTokenService accessTokenService;
private final RiskControlService riskControlService;
private final SeckillRateLimiter seckillRateLimiter;
private final RedisSeckillStockService redisSeckillStockService;
private final SeckillRequestLogRepository requestLogRepository;
private final SeckillMessageProducer seckillMessageProducer;
private final IdGenerator idGenerator;
public SeckillSubmitResult submit(SeckillRequest request) {
if (!accessTokenService.verify(request.activityId(), request.skuId(), request.userId(), request.token())) {
return SeckillSubmitResult.rejected("令牌无效");
}
if (!riskControlService.allow(request.userId(), request.clientIp())) {
requestLogRepository.saveRejected(request, "RISK_REJECTED");
return SeckillSubmitResult.rejected("请求受限");
}
if (!seckillRateLimiter.tryAcquire(request.userId(), request.skuId())) {
requestLogRepository.saveRejected(request, "RATE_LIMITED");
return SeckillSubmitResult.rejected("请求过于频繁");
}
RedisSeckillResult occupyResult = redisSeckillStockService.tryOccupy(request);
if (!occupyResult.success()) {
requestLogRepository.saveRejected(request, occupyResult.code().name());
return SeckillSubmitResult.rejected(occupyResult.message());
}
String requestNo = idGenerator.nextRequestNo();
requestLogRepository.saveQueued(requestNo, request);
seckillMessageProducer.send(new SeckillOrderMessage(
requestNo,
request.activityId(),
request.skuId(),
request.userId()
));
return SeckillSubmitResult.accepted(requestNo);
}
}
这段编排层代码的价值在于把业务流程串起来,同时让每个基础能力都保持独立:
- 令牌校验负责资格
- 风控服务负责风险判断
- 限流器负责频控
- Redis 服务负责原子占位
- MQ 负责解耦落单
这正是“组合能力”而不是“在 Controller 里堆逻辑”。
10.4 Redis 原子占位服务
@Service
public class RedisSeckillStockService {
private static final String OCCUPY_SCRIPT = """
local activityStatus = redis.call('GET', KEYS[3])
if not activityStatus or activityStatus ~= 'ONLINE' then
return 0
end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return 2
end
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) <= 0 then
return 1
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
redis.call('EXPIRE', KEYS[2], 172800)
return 3
""";
private final StringRedisTemplate stringRedisTemplate;
public RedisSeckillStockService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public RedisSeckillResult tryOccupy(SeckillRequest request) {
String stockKey = "seckill:stock:%d:%d".formatted(request.activityId(), request.skuId());
String userKey = "seckill:users:%d:%d".formatted(request.activityId(), request.skuId());
String activityKey = "seckill:activity:status:%d".formatted(request.activityId());
Long result = stringRedisTemplate.execute(
new DefaultRedisScript<>(OCCUPY_SCRIPT, Long.class),
List.of(stockKey, userKey, activityKey),
String.valueOf(request.userId())
);
return switch (result == null ? -1 : result.intValue()) {
case 3 -> RedisSeckillResult.success();
case 2 -> RedisSeckillResult.fail(SeckillRejectCode.DUPLICATE, "请勿重复抢购");
case 1 -> RedisSeckillResult.fail(SeckillRejectCode.SOLD_OUT, "商品已售罄");
case 0 -> RedisSeckillResult.fail(SeckillRejectCode.NOT_OPEN, "活动未开始");
default -> RedisSeckillResult.fail(SeckillRejectCode.SYSTEM_ERROR, "系统繁忙");
};
}
}
10.5 MQ 生产者:削峰而不是同步写库
@Component
public class SeckillMessageProducer {
private final RocketMQTemplate rocketMQTemplate;
public SeckillMessageProducer(RocketMQTemplate rocketMQTemplate) {
this.rocketMQTemplate = rocketMQTemplate;
}
public void send(SeckillOrderMessage message) {
Message<SeckillOrderMessage> mqMessage = MessageBuilder
.withPayload(message)
.setHeader("KEYS", message.requestNo())
.build();
rocketMQTemplate.syncSend("seckill-order-topic", mqMessage, 2000);
}
}
这里的重点不是“把消息发出去”这么简单,而是把同步提交通道和真实落单解耦开。这样 Web 接口只要把资格结果和请求号返回给用户,就能快速结束。
10.6 Consumer:幂等 + 事务 + 结果回写
@Component
@RocketMQMessageListener(topic = "seckill-order-topic", consumerGroup = "seckill-order-group")
public class SeckillOrderConsumer implements RocketMQListener<SeckillOrderMessage> {
private final OrderDomainService orderDomainService;
private final SeckillRequestLogRepository requestLogRepository;
private final ConsumptionIdempotentService consumptionIdempotentService;
private final RedisCompensationService redisCompensationService;
@Override
@Transactional(rollbackFor = Exception.class)
public void onMessage(SeckillOrderMessage message) {
if (!consumptionIdempotentService.markProcessing(message.requestNo())) {
return;
}
try {
String orderNo = orderDomainService.createSeckillOrder(message);
requestLogRepository.markSuccess(message.requestNo(), orderNo);
consumptionIdempotentService.markDone(message.requestNo());
} catch (DuplicateKeyException ex) {
requestLogRepository.markSuccessByDuplicate(message.requestNo());
consumptionIdempotentService.markDone(message.requestNo());
} catch (Exception ex) {
redisCompensationService.rollbackOccupy(message.activityId(), message.skuId(), message.userId());
requestLogRepository.markFailed(message.requestNo(), "ORDER_CREATE_FAILED");
consumptionIdempotentService.markFailed(message.requestNo());
throw ex;
}
}
}
这段代码体现出生产场景里非常关键的三个意识:
- MQ 可能重复投递,所以要消费幂等
- DB 可能插入冲突,所以唯一索引要兜底
- 消费失败后不能只报错,还要回滚占位或进入补偿流程
10.7 订单领域服务:数据库唯一索引是最后防线
@Service
public class OrderDomainService {
private final SeckillOrderMapper seckillOrderMapper;
private final IdGenerator idGenerator;
@Transactional(rollbackFor = Exception.class)
public String createSeckillOrder(SeckillOrderMessage message) {
String orderNo = idGenerator.nextOrderNo();
SeckillOrderDO order = new SeckillOrderDO();
order.setOrderNo(orderNo);
order.setActivityId(message.activityId());
order.setSkuId(message.skuId());
order.setUserId(message.userId());
order.setAmount(new BigDecimal("99.00"));
order.setStatus(0);
seckillOrderMapper.insert(order);
return orderNo;
}
}
上游防重做得再好,也不能替代数据库兜底。因为消息重试、人工回放、补偿任务重放都可能导致重复写入。
10.8 结果查询接口:不要每次都查数据库
@RestController
@RequestMapping("/api/seckill")
public class SeckillResultController {
private final SeckillResultQueryService resultQueryService;
public SeckillResultController(SeckillResultQueryService resultQueryService) {
this.resultQueryService = resultQueryService;
}
@GetMapping("/result/{requestNo}")
public ResponseEntity<SeckillResultView> query(@PathVariable String requestNo,
@RequestHeader("X-User-Id") Long userId) {
return ResponseEntity.ok(resultQueryService.query(requestNo, userId));
}
}
@Service
public class SeckillResultQueryService {
private final StringRedisTemplate redisTemplate;
private final SeckillRequestLogRepository requestLogRepository;
public SeckillResultView query(String requestNo, Long userId) {
String cacheValue = redisTemplate.opsForValue().get("seckill:result:" + requestNo);
if (cacheValue != null) {
return SeckillResultView.from(cacheValue);
}
return requestLogRepository.findByRequestNoAndUserId(requestNo, userId)
.map(SeckillResultView::from)
.orElseGet(SeckillResultView::notFound);
}
}
秒杀结果查询的典型访问量非常大。如果所有请求都直接查 DB,结果查询本身就会成为第二波洪峰。
十一、工程化升级:高并发系统真正靠什么撑住
代码写出来只是开始,真正决定线上稳定性的,是工程化治理。
11.1 多级限流
单层限流远远不够,生产环境一般至少三层:
- Nginx/OpenResty 限流:按 IP、URI 做粗粒度初筛。
- Gateway 限流:按用户、SKU、活动维度做细粒度控制。
- 业务层动态限流:根据热点、风险分、后端 RT 做自适应调节。
建议原则:
- 入口层负责拦大流量
- 业务层负责保护热点资源
- 限流结果要标准化,区分“排队中”“请求过快”“活动拥挤”
11.2 热点 Key 治理
秒杀最典型的热点是某个 SKU 的库存 Key 和资格 Key。
常用手段包括:
- 商品元数据放本地缓存,减少 Redis 读取
- 热点 SKU 使用独立 Redis 分片或专门集群
- 售罄状态快速回推到本地缓存和网关
- 对读热点做 key 打散,对库存主 Key 保持单点原子
这里有个经常被误解的点:
库存 Key 不能为了“分散压力”就随便拆成很多独立分片同时扣减,否则一致性与回收复杂度会急剧上升。绝大多数场景下,更务实的策略是:
- 库存主 Key 保持单点原子
- 热点读流量做打散或本地缓存
11.3 消息削峰与消费扩容
秒杀系统的真实吞吐上限,很多时候取决于 Consumer 的处理能力,而不是 Web 接口能力。
Consumer 设计要点:
- 实例无状态,方便快速扩容
- 按活动或 SKU 做合理分区,避免单分区热点
- 单条消息处理逻辑短小,不串远程慢调用
- 配置重试次数和死信队列,防止无限重试
11.4 事务边界与可靠消息
这是很多文章讲得不够深入的一点。
秒杀系统里至少有三个关键动作:
- Redis 占位成功
- MQ 消息投递成功
- DB 订单写入成功
这三者不可能天然强一致,所以必须明确事务边界。
一种常见做法是:
- 先 Redis 占位
- 再投 MQ
- Consumer 再落库
- 如果投 MQ 失败,触发占位回滚
- 如果落库失败,触发补偿或重试
如果业务对消息可靠性要求更高,可以进一步引入:
设计时不要追求“绝对完美一致”,而要追求“失败可恢复、状态可追踪、补偿可执行”。
11.5 线程池与连接池隔离
很多系统不是库存扣减崩掉,而是线程池、连接池、下游资源被拖死。
必须做的隔离包括:
- 秒杀专属 Web 线程池
- 秒杀专属 Redis 连接池
- 秒杀专属 MQ Producer/Consumer 线程池
- 秒杀专属数据库连接池甚至专属库实例
否则秒杀一来,普通下单、支付、商品查询一起被拖垮。
11.6 降级策略
不是所有故障都要硬抗,很多时候要主动退让。
常见降级手段包括:
- 只保留预约用户入口
- 热门 SKU 临时关闭展示
- 接口统一返回“排队中”,拉长结果回查时间
- 关闭推荐、埋点、个性化信息等非核心依赖
- 售罄后直接静态返回,避免无效流量穿透
高并发系统真正成熟的标志,不是从不降级,而是降级动作设计得足够快、足够清晰、足够可控。
十二、反作弊与风控:没有这一层,系统再快也没意义
秒杀系统天然会吸引黄牛和脚本流量。没有风控的高并发系统,本质上只是给脚本搭了高速公路。
12.1 常见攻击手段
- 脚本高频请求
- 代理 IP 池轮换
- 批量注册账号
- 模拟器与设备农场
- 逆向前端接口与签名逻辑
- 提前抢和重放攻击
12.2 风控设计建议
至少从以下四个维度联防:
- 账号维度:新号、低活跃号、异常注册批次
- 设备维度:设备指纹、模拟器识别、Root/Jailbreak 特征
- 网络维度:IP 信誉、代理特征、地域异常
- 行为维度:点击路径、停留时长、批量一致性、时间节奏
12.3 资格令牌机制
非常实用的一个方案是“预约成功后发放抢购令牌”。
令牌建议包含以下字段:
activityId
skuId
userId
expireAt
nonce
- HMAC 签名
令牌的价值在于:
- 限定只有预约用户能进入核心链路
- 可以设置一次性、短时有效、防重放
- 可在网关层快速校验
12.4 时间一致性与提前抢防护
不要相信客户端时间。活动开始时间必须以服务端时钟为准。
常见做法:
- 秒杀开始前不下发合法令牌
- 服务端再次校验开始时间
- 客户端页面倒计时只用于展示,不作为判定依据
对于时间敏感型活动,最好统一:
- 网关机器和应用机器 NTP 同步
- 活动切换由统一控制面发布
- Redis 中的活动状态切换有明确时间窗口
否则“有的机器觉得开始了,有的机器觉得没开始”会带来非常隐蔽的问题。
十三、可观测性与运维体系:稳定不是写出来的,是观测出来的
大促现场不是做技术炫技,而是做信息战。谁先看清问题,谁就更有机会把事故压住。
13.1 必须监控的指标
流量指标
- 每秒请求量 QPS
- 每秒有效请求数
- 接口成功率
- 限流命中率
- 风控拦截率
缓存指标
- Redis QPS
- Redis 慢查询
- Lua 执行耗时
- 连接池使用率
- 热点 Key 命中情况
消息指标
- Topic 写入 TPS
- Consumer 消费 TPS
- 消息积压数
- 重试次数
- 死信队列数量
数据库指标
- 插入 TPS
- 活跃连接数
- 行锁等待
- 慢 SQL
- 主从延迟
业务指标
- 已占位人数
- 已成功下单人数
- 售罄时间点
- 重复请求数
- 查询结果失败率
13.2 Trace 设计建议
每次秒杀请求至少要贯穿:
traceId
requestNo
userId
activityId
skuId
messageId
这样出现“前端显示成功但查不到订单”时,才能从网关一路追到 MQ、Consumer 和数据库。
13.3 库存对账机制
生产环境一定要有库存对账任务,用于发现:
- Redis 库存与 DB 已售不一致
- 回滚失败导致的库存丢失
- 异常消息重试造成的重复占位
示例代码:
@Scheduled(fixedDelay = 30000)
public void reconcile() {
List<StockSnapshot> snapshots = stockSnapshotRepository.findActiveSnapshots();
for (StockSnapshot snapshot : snapshots) {
int sold = seckillOrderMapper.countPaidOrCreated(snapshot.getActivityId(), snapshot.getSkuId());
int expectedAvailable = snapshot.getInitialStock() - sold;
int redisAvailable = redisStockRepository.getAvailable(snapshot.getActivityId(), snapshot.getSkuId());
if (expectedAvailable != redisAvailable) {
log.error("stock mismatch, activityId={}, skuId={}, expected={}, actual={}",
snapshot.getActivityId(), snapshot.getSkuId(), expectedAvailable, redisAvailable);
}
}
}
对账任务不一定立刻自动修复,但一定要及时发现并触发告警。
13.4 告警策略不要只盯技术指标
很多团队只盯 CPU、内存、RT,忽视业务告警。
秒杀系统更关键的告警往往是:
- 售罄后仍有大量成功占位
- 请求结果查询失败率异常升高
- 风控拦截率突然大幅下降或升高
- MQ 堆积增长快于 Consumer 扩容速度
- 某个 SKU 的库存漂移开始出现
因为这些指标更接近业务真实性。
十四、Kubernetes 与弹性部署实践
把秒杀系统部署到 Kubernetes 时,重点不只是“起多少 Pod”,而是:
- 如何快速扩容
- 如何优雅缩容
- 如何避免滚动发布误伤活动流量
14.1 典型部署建议
| 服务 |
部署建议 |
| Gateway |
多副本,跨可用区部署,优先保证入口可用 |
| Seckill Web |
无状态,按 CPU + QPS 双指标扩容 |
| Consumer |
按消息堆积和消费延迟扩容 |
| Redis |
独立集群,避免与普通业务混部 |
| MQ |
Broker 高可用部署,配置磁盘、堆积和重试告警 |
| DB |
主从或分片部署,活动前压测连接池上限 |
14.2 HPA 示例
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: seckill-web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: seckill-web
minReplicas: 8
maxReplicas: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
- type: Pods
pods:
metric:
name: seckill_request_qps
target:
type: AverageValue
averageValue: "1500"
14.3 优雅上下线
活动期间最怕 Pod 被滚动发布直接摘掉,导致正在处理的请求失败。
建议:
- 开启
readinessProbe
preStop 中先摘流量再等待
- Consumer 先停止拉取,再等当前消息处理完
- 活动窗口内尽量冻结发布
示例:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"]
14.4 服务网格是否一定需要
不是所有秒杀系统都必须上 Istio 或 Service Mesh。
如果你当前阶段更关注:
那么 OpenResty + Gateway + K8s 已经足够。
Service Mesh 更适合在以下场景引入:
- 多服务调用关系复杂
- 灰度和流量治理要求高
- 统一 mTLS、审计和流量镜像有明确价值
架构不是组件越多越先进,而是复杂度和收益要匹配。
十五、容量评估与压测方法
高并发系统不是靠“感觉能扛住”上线,而是靠容量估算和压测数据说话。
15.1 容量估算思路
假设:
- 预约用户 300 万
- 开场 10 秒内有 20% 用户点击
- 60% 请求在前端或 CDN 层被拦截
- 剩余 40% 到达网关
则:
- 原始点击数约 60 万
- 10 秒内平均约 6 万 QPS
- 峰值通常是平均值的 2 到 3 倍
因此核心链路应至少按 12 万到 18 万 QPS 峰值进行设计。
15.2 压测要分层做
不要只压一个 HTTP 接口,应该按层验证:
- 网关压测:验证限流、签名、熔断策略
- Redis 压测:验证 Lua 吞吐与 RT
- MQ 压测:验证写入与消费能力
- Consumer 压测:验证订单落库能力
- 端到端压测:验证真实业务成功率
15.3 压测关注项
- 是否出现库存负数
- 是否出现重复订单
- MQ 是否持续积压
- 结果查询是否压垮缓存或 DB
- Consumer 扩容是否跟得上
- 活动开始后监控是否在 1 分钟内收敛到可读状态
15.4 压测脚本示例
wrk -t16 -c10000 -d30s \
-s flash_sale.lua \
--latency \
http://gateway.flash-sale.io/api/v1/flash-sale/order
wrk.method = "POST"
wrk.body = '{"skuId":1001,"quantity":1}'
wrk.headers["Content-Type"] = "application/json"
wrk.headers["X-User-Id"] = tostring(math.random(1, 1000000))
15.5 看压测结果时,不要只看 QPS
很多团队只看一个数字:“打到了多少 QPS”。
但秒杀压测至少要同时看五个维度:
- 正确性:库存是否准确,是否有重复单
- 延迟:P95、P99 是否失控
- 稳定性:压测时间拉长后是否出现抖动
- 可恢复性:压完之后系统是否能快速恢复
- 观测性:是否能通过监控快速识别瓶颈
真正高质量的压测,不是把机器打红,而是把风险打出来。
十六、真实故障场景与应对策略
场景一:Redis 突然抖动
表现:
- 接口 RT 猛增
- Lua 执行超时
- 业务层大量失败
处理:
- 网关限流立即收紧
- 热点 SKU 降级为排队模式
- 只对预约白名单放量
- 临时关闭非核心接口
场景二:MQ 堆积快速上升
表现:
- 抢购接口还在成功返回“排队中”
- 用户查结果越来越慢
处理:
- 扩容 Consumer
- 排查是否存在热点分区
- 检查下游 DB 慢 SQL 和唯一键冲突
- 必要时暂停新流量进入,优先消化积压
场景三:数据库写入异常
表现:
处理:
- 启动补偿任务回滚 Redis 占位
- 请求状态标记“系统繁忙,请稍后重试”
- 如果故障持续,直接关闭抢购入口,保留结果查询
场景四:热点 SKU 售罄后仍有大量请求
表现:
- Redis 与 Gateway 仍承受大量无效流量
处理:
- 将售罄状态同步到本地缓存和 CDN 页面
- 前端按钮置灰
- 网关层对售罄 SKU 直接静态返回
场景五:风控服务异常导致链路变慢
表现:
处理:
- 风控服务降级为本地缓存规则或基础黑名单模式
- 将复杂实时画像检查切换为异步审计
- 保留最基本的账号/IP 黑名单校验
这类场景说明:秒杀系统不是所有依赖都必须 100% 实时在线,核心是分清“必要能力”和“可退化能力”。
十七、一个更贴近真实项目的落地案例
17.1 案例背景
某数码商城在新品发售日做限量秒杀:
- 商品:旗舰耳机
- 限量库存:10,000 件
- 预约用户:120 万
- 开场峰值:14 万 QPS
17.2 初版方案暴露的问题
最初团队采用的是:
- Redis 扣库存
- 同步写 MySQL 订单
- 没有独立结果查询缓存
压测后暴露出三个核心问题:
- 数据库连接池被瞬间打满。
- 用户重复点击导致重复写库请求激增。
- 售罄后仍有大量无效流量穿透到应用层。
17.3 升级方案
团队做了四项关键改造:
- 抢购资格改为预约令牌校验。
- Redis Lua 原子占位替换散落式判断。
- 同步落单改为 MQ 异步消费。
- 售罄状态同步到网关和前端静态页。
17.4 结果
压测结果从:
提升到:
- 异步削峰方案:65,000 QPS,P99 58ms
线上活动中:
- 10,000 件库存 7 秒售罄
- 未出现超卖
- 订单成功率稳定在 99.95% 以上
- 剩余失败主要来自重复点击和风控拦截
这个案例说明,秒杀优化的重点从来不是“把一个 SQL 调快”,而是把无效流量挡在外面,把有效请求做成异步状态机。
十八、上线前 Checklist
- 活动数据、库存、状态、令牌已全部预热到 Redis
- 秒杀链路与主交易链路的线程池、连接池、限流规则已隔离
- 网关、业务、风控多层限流已开启
- Redis Lua 脚本已压测并验证原子性
- MQ 堆积、重试、死信告警已配置
- 订单唯一索引与消费幂等已验证
- 请求流水、结果缓存、结果查询已联调通过
- 库存对账与补偿任务已演练
- 售罄后的前端、网关、缓存收敛逻辑已验证
- K8s 扩容、缩容、优雅上下线已验证
- 监控大盘、值班手册、故障预案、回滚预案已准备完毕
十九、容易被忽视但决定质量的几个细节
19.1 请求号必须统一贯穿全链路
如果 Web、MQ、Consumer、DB 没有统一的 requestNo,你会很难排查问题。
19.2 不要在高峰期做复杂同步远程调用
很多系统最终不是倒在库存,而是倒在:
- 风控 RPC
- 营销服务 RPC
- 商品中心 RPC
- 查询链路同步读取从库
秒杀期间,核心链路要尽量短、尽量近、尽量少依赖。
19.3 不要把库存事实只放在 Redis
Redis 适合扛并发,不适合作为唯一审计事实。
库存快照、请求流水、订单事实仍应在 DB 中保留,供对账和复盘使用。
19.4 结果查询比很多人想象中更重要
大量用户在秒杀后最频繁的动作不是继续下单,而是疯狂刷新结果。
如果不提前设计结果缓存和查询接口,活动后的第二波查询流量会把系统再次打穿。
二十、文章结论:一套真正生产可用的秒杀系统应该具备什么
如果要用一句话总结秒杀系统的架构本质,那就是:
在正确性绝不退让的前提下,把绝大多数请求尽早淘汰,把少量有效请求异步化处理。
一套成熟的高并发秒杀系统,至少应该具备下面这些能力:
- 入口抗洪峰:CDN、WAF、限流、风控联防
- 业务抗热点:本地缓存、Redis Lua、热点资源治理
- 后端抗冲击:MQ 削峰、无状态 Consumer、分库分表
- 数据保正确:多层幂等、唯一索引、补偿与对账
- 运维可落地:预热、扩容、降级、观测、演练、复盘
很多人把秒杀理解为“高并发优化案例”,但在真正的工程实践里,它更像是一门综合架构能力考试。它考验的不只是 Redis、MQ、MySQL 这些组件会不会用,更考验你能否把系统设计成:
- 高峰时不崩
- 故障时可控
- 售罄后可收敛
- 复盘时可追溯
这才是 云栈社区 上众多架构师讨论的生产级高并发系统的核心标准。
二十一、延伸阅读方向
如果你希望继续把这套系统做深,下一步可以重点研究:
- 单元化架构与异地多活
- OpenResty 前置校验与边缘风控
- Redis Cluster 热点治理与大 Key 治理
- RocketMQ 事务消息、顺序消息与死信治理
- 分库分表后的链路追踪与对账体系
- 大促场景下的混沌工程与故障演练
- 资格预约、支付闭环与超时释放的一体化设计
附录 A:本地开发环境示例
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: flash_sale
ports:
- "3306:3306"
redis:
image: redis:7.0
command: redis-server --appendonly yes
ports:
- "6379:6379"
rocketmq-namesrv:
image: apache/rocketmq:5.1.3
command: sh mqnamesrv
ports:
- "9876:9876"
rocketmq-broker:
image: apache/rocketmq:5.1.3
command: sh mqbroker -n localhost:9876
ports:
- "10909:10909"
- "10911:10911"
depends_on:
- rocketmq-namesrv
附录 B:示例 SQL
CREATE TABLE `flash_sale_item` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sku_id` BIGINT NOT NULL COMMENT '商品 SKU ID',
`title` VARCHAR(256) NOT NULL COMMENT '商品标题',
`price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
`original_price` DECIMAL(10,2) NOT NULL COMMENT '原价',
`stock` INT NOT NULL DEFAULT 0 COMMENT '库存数量',
`limit_per_user` INT NOT NULL DEFAULT 1 COMMENT '限购数量',
`start_time` DATETIME NOT NULL COMMENT '秒杀开始时间',
`end_time` DATETIME NOT NULL COMMENT '秒杀结束时间',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0-待开始, 1-进行中, 2-已结束',
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_start_time` (`start_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
如果把秒杀系统设计成一场“分层拦截无效请求、保护核心资源、最终异步确认状态”的工程协同,那么高并发就不再只是压力,而会变成一套可预测、可控制、可复用的架构能力。