找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3900

积分

0

好友

510

主题
发表于 1 小时前 | 查看: 4| 回复: 0

秒杀系统真正难的,从来不是“把库存减掉”,而是在瞬时洪峰、热点商品、恶意流量、链路抖动和多组件故障同时出现时,依然做到不超卖、不拖垮主站、可快速扩容、可回溯复盘。

摘要

很多团队把秒杀当成“下单接口性能优化”问题,结果上线后发现真正被考验的并不只是 SQL 或 Redis,而是整套系统的流量治理能力、状态建模能力、异步解耦能力、故障隔离能力和运营保障能力。

一套生产级秒杀系统,本质上是在做四件事:

  1. 尽可能早地淘汰无效请求。
  2. 尽可能少地让请求进入数据库。
  3. 把同步强依赖改造成可控的异步状态机。
  4. 在极端 高并发 下,优先保证正确性和核心链路可用性。

本文从真实业务场景出发,系统讲清楚秒杀系统的原理、架构、工程化实现与生产治理,重点覆盖以下内容:

  • 秒杀系统为什么不能只靠数据库扣库存
  • 为什么“抢购成功”通常只是拿到资格,而不是立即落单
  • Redis Lua、MQ 削峰、幂等、补偿、对账各自解决什么问题
  • 如何支撑热点 SKU、高并发、恶意刷流量和多组件故障
  • 如何做可上线的代码设计、容量评估、压测、观测与值班预案

如果你希望把一篇“能讲概念”的秒杀文章升级成“架构师能拿去做设计评审”的生产级稿件,这篇文章就是按这个目标写的。


一、为什么秒杀系统值得单独设计

秒杀不是普通下单的流量放大版,而是一个结构性不同的问题。

它同时具备以下几个特征:

  • 流量极端不均匀:平时几百 QPS,开场瞬间可能冲到数万到数十万 QPS。
  • 热点高度集中:90% 的请求可能打在 1 到 3 个热门 SKU 上。
  • 用户容错极低:抢不到可以接受,但超卖、重复下单、付款后无单不可接受。
  • 活动窗口极短:故障恢复时间以秒计算,而不是小时。
  • 黑灰产密集:脚本刷号、代理池、打码平台、设备农场都会出现。
  • 链路影响面大:活动流量一旦失控,常常拖垮商品、购物车、支付、订单查询等主站链路。

所以,秒杀系统不能只追求“吞吐高”,而要同时满足四个目标:

  1. 正确性优先:库存不能超卖,用户维度不能重复下单。
  2. 系统隔离:秒杀链路不能拖垮交易主站。
  3. 无效请求前置淘汰:越早拦截越好。
  4. 工程可运营:能预热、能扩容、能降级、能对账、能复盘。

换句话说,秒杀系统考验的不是某个中间件用得熟不熟,而是你能不能把一个极端热点问题改造成一套可控的分层协同系统。


二、目标场景与非功能性约束

为了避免方案只停留在概念层,先定义一个更接近真实业务的活动场景。

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 个字:

前置过滤,缓存承压,消息削峰,异步落单。

很多新手方案的思路是:

用户请求 -> 应用服务 -> 数据库扣库存 -> 创建订单 -> 返回结果

这个流程在普通下单场景没问题,但在秒杀场景会迅速暴露出三个问题:

  1. 数据库成为并发热点,连接池和行锁会被瞬间打满。
  2. 无效请求和有效请求混在一起,浪费核心资源。
  3. 同步链路太长,任何一个依赖抖动都会放大成整体雪崩。

生产级秒杀系统会改造成:

用户请求
  -> 入口限流/风控/资格校验
  -> Redis 原子占位
  -> MQ 排队削峰
  -> Consumer 异步落单
  -> 前端轮询或推送结果

这背后的结构变化有两个关键点。

3.1 从“同步事务思维”切换到“状态机思维”

秒杀不是一次单纯的同步调用,而是一条跨组件状态流转链路。一次请求通常会经历下面几个状态:

INIT
  -> TOKEN_VERIFIED
  -> RISK_PASSED
  -> STOCK_OCCUPIED
  -> MQ_QUEUED
  -> ORDER_CREATED
  -> SUCCESS

如果某个阶段失败,还会进入:

REJECTED
FAILED
COMPENSATING
ROLLED_BACK

也就是说,秒杀系统真正要设计的不是一个接口,而是一套可追踪、可补偿、可审计的状态机。

3.2 从“所有请求都进核心链路”切换到“分层淘汰”

成熟系统通常只允许极少数有效请求进入最重的链路。

分层淘汰顺序通常如下:

  1. 入口层淘汰:未登录、签名错误、无资格、明显异常流量直接拒绝。
  2. 缓存层淘汰:活动未开始、库存售罄、重复抢购在 Redis 阶段快速结束。
  3. 消息层削峰:拿到资格的请求先排队,不立即写库。
  4. 消费层落单:异步串起订单创建、结果回写和补偿。
  5. 查询层回查:前端通过轮询、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 重复请求必须在最前面拦截

用户会重复点击,脚本会高频重放,客户端会超时重试。

如果幂等只依赖数据库唯一键,数据库虽然最终能兜底,但已经被重复请求拖死了。因此幂等至少要做四层:

  1. 网关层限流
  2. Redis 用户抢购标记
  3. MQ 消费幂等
  4. 数据库唯一索引兜底

6.4 秒杀系统默认接受最终一致性

Redis、MQ、MySQL 三者之间做跨组件强事务,理论上可以更“严谨”,但工程上通常非常昂贵,而且容易把高可用做没。

多数成熟系统采取的是:

  • Redis 先占位
  • MQ 异步下单
  • DB 落单成功后回写结果
  • 异常时通过补偿回滚占位或修正状态

这是以系统可用性和吞吐优先的最终一致性架构。

6.5 宁可少卖,不可超卖

在 Redis 抖动、MQ 不稳定、DB 瞬时故障等场景下,秒杀系统的默认策略应该是保守:

  • 超时直接拒绝或返回排队中
  • 不冒险“猜测成功”
  • 以用户补偿代替系统失控

因为少卖可以运营补偿,超卖往往是品牌事故。


七、一次秒杀请求究竟经历了什么

7.1 活动开始前:预热阶段

秒杀系统稳定性的很大一部分,来自活动前准备而不是活动开始后的在线处理。

预热阶段通常要完成以下动作:

  1. 将活动信息、SKU、库存、限购规则加载到 Redis。
  2. 将静态页、商品详情、倒计时资源推送到 CDN。
  3. 生成预约资格或抢购令牌。
  4. 扩容 Gateway、Seckill Service、Consumer。
  5. 验证风控、Redis、MQ、DB 的容量与告警阈值。
  6. 准备降级开关、只读页和人工预案。

7.2 用户点击抢购:同步快速返回阶段

同步链路应尽量短,只做高价值判断:

  1. 校验登录态和活动状态
  2. 校验资格令牌和签名
  3. 做风控判断和频控判断
  4. 用 Redis Lua 原子占位
  5. 生成请求号并投递 MQ
  6. 返回排队中结果

注意,这一阶段的目标不是“创建订单”,而是“尽快且准确地决定该请求是否值得进入队列”。

7.3 后台异步消费:真实落单阶段

Consumer 承担真正的订单创建工作:

  1. 按消息唯一 ID 做消费幂等
  2. 校验 DB 中是否已有该用户该活动该 SKU 的订单
  3. 写订单、明细、审计流水
  4. 更新请求状态和结果缓存
  5. 必要时触发支付链路或通知链路

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

很多错误实现会这样做:

  1. 先判断库存是否大于 0
  2. 再做 DECR
  3. 再写用户抢购标记

问题在于这三步如果不在 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 多级限流

单层限流远远不够,生产环境一般至少三层:

  1. Nginx/OpenResty 限流:按 IP、URI 做粗粒度初筛。
  2. Gateway 限流:按用户、SKU、活动维度做细粒度控制。
  3. 业务层动态限流:根据热点、风险分、后端 RT 做自适应调节。

建议原则:

  • 入口层负责拦大流量
  • 业务层负责保护热点资源
  • 限流结果要标准化,区分“排队中”“请求过快”“活动拥挤”

11.2 热点 Key 治理

秒杀最典型的热点是某个 SKU 的库存 Key 和资格 Key。

常用手段包括:

  • 商品元数据放本地缓存,减少 Redis 读取
  • 热点 SKU 使用独立 Redis 分片或专门集群
  • 售罄状态快速回推到本地缓存和网关
  • 对读热点做 key 打散,对库存主 Key 保持单点原子

这里有个经常被误解的点:

库存 Key 不能为了“分散压力”就随便拆成很多独立分片同时扣减,否则一致性与回收复杂度会急剧上升。绝大多数场景下,更务实的策略是:

  • 库存主 Key 保持单点原子
  • 热点读流量做打散或本地缓存

11.3 消息削峰与消费扩容

秒杀系统的真实吞吐上限,很多时候取决于 Consumer 的处理能力,而不是 Web 接口能力。

Consumer 设计要点:

  • 实例无状态,方便快速扩容
  • 按活动或 SKU 做合理分区,避免单分区热点
  • 单条消息处理逻辑短小,不串远程慢调用
  • 配置重试次数和死信队列,防止无限重试

11.4 事务边界与可靠消息

这是很多文章讲得不够深入的一点。

秒杀系统里至少有三个关键动作:

  1. Redis 占位成功
  2. MQ 消息投递成功
  3. DB 订单写入成功

这三者不可能天然强一致,所以必须明确事务边界。

一种常见做法是:

  • 先 Redis 占位
  • 再投 MQ
  • Consumer 再落库
  • 如果投 MQ 失败,触发占位回滚
  • 如果落库失败,触发补偿或重试

如果业务对消息可靠性要求更高,可以进一步引入:

  • RocketMQ 事务消息
  • 本地消息表 + Outbox
  • 请求流水扫描补投

设计时不要追求“绝对完美一致”,而要追求“失败可恢复、状态可追踪、补偿可执行”。

11.5 线程池与连接池隔离

很多系统不是库存扣减崩掉,而是线程池、连接池、下游资源被拖死。

必须做的隔离包括:

  • 秒杀专属 Web 线程池
  • 秒杀专属 Redis 连接池
  • 秒杀专属 MQ Producer/Consumer 线程池
  • 秒杀专属数据库连接池甚至专属库实例

否则秒杀一来,普通下单、支付、商品查询一起被拖垮。

11.6 降级策略

不是所有故障都要硬抗,很多时候要主动退让。

常见降级手段包括:

  • 只保留预约用户入口
  • 热门 SKU 临时关闭展示
  • 接口统一返回“排队中”,拉长结果回查时间
  • 关闭推荐、埋点、个性化信息等非核心依赖
  • 售罄后直接静态返回,避免无效流量穿透

高并发系统真正成熟的标志,不是从不降级,而是降级动作设计得足够快、足够清晰、足够可控。


十二、反作弊与风控:没有这一层,系统再快也没意义

秒杀系统天然会吸引黄牛和脚本流量。没有风控的高并发系统,本质上只是给脚本搭了高速公路。

12.1 常见攻击手段

  • 脚本高频请求
  • 代理 IP 池轮换
  • 批量注册账号
  • 模拟器与设备农场
  • 逆向前端接口与签名逻辑
  • 提前抢和重放攻击

12.2 风控设计建议

至少从以下四个维度联防:

  1. 账号维度:新号、低活跃号、异常注册批次
  2. 设备维度:设备指纹、模拟器识别、Root/Jailbreak 特征
  3. 网络维度:IP 信誉、代理特征、地域异常
  4. 行为维度:点击路径、停留时长、批量一致性、时间节奏

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 接口,应该按层验证:

  1. 网关压测:验证限流、签名、熔断策略
  2. Redis 压测:验证 Lua 吞吐与 RT
  3. MQ 压测:验证写入与消费能力
  4. Consumer 压测:验证订单落库能力
  5. 端到端压测:验证真实业务成功率

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”。

但秒杀压测至少要同时看五个维度:

  1. 正确性:库存是否准确,是否有重复单
  2. 延迟:P95、P99 是否失控
  3. 稳定性:压测时间拉长后是否出现抖动
  4. 可恢复性:压完之后系统是否能快速恢复
  5. 观测性:是否能通过监控快速识别瓶颈

真正高质量的压测,不是把机器打红,而是把风险打出来。


十六、真实故障场景与应对策略

场景一:Redis 突然抖动

表现:

  • 接口 RT 猛增
  • Lua 执行超时
  • 业务层大量失败

处理:

  • 网关限流立即收紧
  • 热点 SKU 降级为排队模式
  • 只对预约白名单放量
  • 临时关闭非核心接口

场景二:MQ 堆积快速上升

表现:

  • 抢购接口还在成功返回“排队中”
  • 用户查结果越来越慢

处理:

  • 扩容 Consumer
  • 排查是否存在热点分区
  • 检查下游 DB 慢 SQL 和唯一键冲突
  • 必要时暂停新流量进入,优先消化积压

场景三:数据库写入异常

表现:

  • Consumer 重试增多
  • 订单创建失败率升高

处理:

  • 启动补偿任务回滚 Redis 占位
  • 请求状态标记“系统繁忙,请稍后重试”
  • 如果故障持续,直接关闭抢购入口,保留结果查询

场景四:热点 SKU 售罄后仍有大量请求

表现:

  • Redis 与 Gateway 仍承受大量无效流量

处理:

  • 将售罄状态同步到本地缓存和 CDN 页面
  • 前端按钮置灰
  • 网关层对售罄 SKU 直接静态返回

场景五:风控服务异常导致链路变慢

表现:

  • 主接口 RT 上升
  • 风控调用超时增多

处理:

  • 风控服务降级为本地缓存规则或基础黑名单模式
  • 将复杂实时画像检查切换为异步审计
  • 保留最基本的账号/IP 黑名单校验

这类场景说明:秒杀系统不是所有依赖都必须 100% 实时在线,核心是分清“必要能力”和“可退化能力”。


十七、一个更贴近真实项目的落地案例

17.1 案例背景

某数码商城在新品发售日做限量秒杀:

  • 商品:旗舰耳机
  • 限量库存:10,000 件
  • 预约用户:120 万
  • 开场峰值:14 万 QPS

17.2 初版方案暴露的问题

最初团队采用的是:

  • Redis 扣库存
  • 同步写 MySQL 订单
  • 没有独立结果查询缓存

压测后暴露出三个核心问题:

  1. 数据库连接池被瞬间打满。
  2. 用户重复点击导致重复写库请求激增。
  3. 售罄后仍有大量无效流量穿透到应用层。

17.3 升级方案

团队做了四项关键改造:

  1. 抢购资格改为预约令牌校验。
  2. Redis Lua 原子占位替换散落式判断。
  3. 同步落单改为 MQ 异步消费。
  4. 售罄状态同步到网关和前端静态页。

17.4 结果

压测结果从:

  • 同步方案:8,000 QPS,P99 320ms

提升到:

  • 异步削峰方案: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 这些组件会不会用,更考验你能否把系统设计成:

  • 高峰时不崩
  • 故障时可控
  • 售罄后可收敛
  • 复盘时可追溯

这才是 云栈社区 上众多架构师讨论的生产级高并发系统的核心标准。


二十一、延伸阅读方向

如果你希望继续把这套系统做深,下一步可以重点研究:

  1. 单元化架构与异地多活
  2. OpenResty 前置校验与边缘风控
  3. Redis Cluster 热点治理与大 Key 治理
  4. RocketMQ 事务消息、顺序消息与死信治理
  5. 分库分表后的链路追踪与对账体系
  6. 大促场景下的混沌工程与故障演练
  7. 资格预约、支付闭环与超时释放的一体化设计

附录 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='秒杀商品表';

如果把秒杀系统设计成一场“分层拦截无效请求、保护核心资源、最终异步确认状态”的工程协同,那么高并发就不再只是压力,而会变成一套可预测、可控制、可复用的架构能力。




上一篇:Redis 为何能扛百万并发?从事件循环、内存编码到集群治理的全栈解析
下一篇:我用Qwen-3.7-max做了个播客筛选器:从想法到可用的真实过程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-24 21:47 , Processed in 0.859322 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表