百万级QPS秒杀系统架构设计方案学习指南

Cosolar 5 阅读 架构与设计

百万级 QPS 秒杀系统架构设计方案学习指南

image.png

一、系统概述与挑战

在深入技术细节之前,我们需要先理解秒杀系统的业务本质。秒杀并不是简单的「在短时间内很多人买东西」,它的技术难点与普通电商有本质区别。普通电商关注的是转化率、客单价、用户体验;而秒杀系统在这些问题之上,还要解决一个更根本的挑战:如何在超大规模并发下,保证系统不挂、数据不错、体验不差

很多工程师在设计秒杀系统时容易陷入一个误区——把注意力集中在「优化性能」上。实际上,秒杀的核心矛盾不是「如何让每个请求都成功」,而是「如何优雅地让绝大多数请求失败」。因为秒杀永远是供不应求的,库存 1000 件、100 万人抢购,99.9% 的请求最终都会失败。把这部分失败请求在尽可能浅的层次上拦截掉,才是秒杀系统的设计哲学。

接下来从业务场景、核心挑战、设计目标三个维度,逐步构建对秒杀系统的全局认知。

1.1 业务场景

秒杀系统是一种在极短时间内面临超高并发请求的电商业务场景。典型特点:

  • 流量瞬间爆发:活动开始瞬间,QPS 从平日几百暴涨到百万级
  • 有效流量小:真正能秒杀到商品的用户可能只有 1/1000
  • 库存有限:商品数量极少(几十到几百件),参与人数极多(百万级甚至千万级)
  • 强一致性要求:不能超卖,不能少卖
  • 体验要求高:用户期望毫秒级响应

1.2 核心挑战

从技术的角度看,秒杀系统的每一个挑战都对应着一个架构层面的核心决策。下面我们逐项拆解。

挑战维度 具体问题 应对架构策略
流量洪峰 入口处 QPS 到达 100W+,远超系统承载能力 逐层过滤——CDN→网关限流→Token→Redis预扣→MQ→DB
超卖与少卖 高并发下库存扣减的并发安全与一致性问题 Redis Lua原子脚本 + 库存分桶 + DB乐观锁三层防线
热点数据 所有请求集中在少量商品行,缓存/DB 压力极度倾斜 本地缓存过滤 + 库存分桶分散热点 + DB读写分离
用户体验 需快速给用户反馈结果,不能让用户长时间等待 异步下单——秒杀成功后即返回,MQ异步创建订单
系统可用性 秒杀流量可能打垮整个系统的其他业务链 独立部署 + Sentinel熔断 + 多级降级预案
防刷与公平 防止机器人、脚本刷单,保障真实用户体验 行为模式识别 + 账号风险评级 + 业务规则过滤

从上表可以看出,秒杀系统的设计本质上是一个「用最小代价筛选出有效流量」的系统工程。每一个架构决策都有其对应的问题域,没有一劳永逸的方案。我们在后续章节中会看到,这个漏斗思维贯穿始终。

1.3 设计目标

  • 峰值 QPS:入口 1,000,000,实际下单链路承受 100,000+ QPS
  • 库存准确性:零超卖、零少卖
  • 响应延迟:网关层 < 10ms,核心交易 < 200ms
  • 系统可用性:99.99%(全年不可用时间不超过 1 小时)
  • 降级能力:极端情况下核心链路仍可降级运行

二、整体架构设计

在正式绘制架构图之前,我们需要理解一个根本性的原则:整体架构不是把组件堆在一起,而是定义流量如何在各层之间被逐级筛选的规则体系。秒杀系统之所以复杂,不是因为它用了很多组件,而是因为它在每个环节都需要做出精确的取舍——在哪里拦截、在哪里放行、在哪里校验、在哪里落库,每一层的设计失误都会被下一层放大,最终导致系统崩溃。

接下来我们从架构全图开始,然后将这个庞大的系统拆解为几个关键的分层原则,让读者先建立完整的心智模型,后续章节再深入每一层的技术细节。

2.1 架构全图

diagram.svg

2.2 分层架构设计原则

秒杀系统的分层设计哲学可以概括为一句话:「让无效流量以最小的代价死在路上」。这听起来有点黑色幽默,但它精确地描述了秒杀系统的本质。数据库的写入能力通常在 5000~10000 TPS 级别,而入口处有 100W QPS 的洪峰涌入,这意味着 99% 的请求注定无法成交。如果不在前几层就挡掉这些无效流量,它们就会一路冲到数据库,把整个存储层打爆。因此,我们在设计每一层时都要回答一个问题:这一层的过滤效率有多高?每个请求的处理成本有多低?

为了更直观地理解这种漏斗效应,我们可以用一个数据流模型来描述:

┌─────────────────────────────────────────────────┐
│ 逐层过滤,流量漏斗模型 │
│ │
│ 1000W QPS ── CDN/静态化 ──▶ 过滤 90% │
│ 100W QPS ── 网关限流 ──▶ 过滤 80% │
│ 20W QPS ── 前置过滤 ──▶ 过滤 50% │
│ 10W QPS ── Redis预扣 ──▶ 过滤 99% │
│ 0.1W QPS ── MQ 下单 ──▶ 真正下单量 │
│ │
└─────────────────────────────────────────────────┘

核心设计理念:漏斗形逐层过滤,每一层都尽可能拦截无效流量,让最终到达数据库的 QPS 控制在万级以内。

2.3 技术选型

层级 技术选型 选型理由
CDN 阿里云/腾讯云 CDN 静态资源缓存、边缘就近访问
负载均衡 LVS + Nginx LVS 四层高性能 + Nginx 七层灵活
API 网关 OpenResty (Lua) / APISIX/SpringCloud Gateway 高性能 + 可编程限流
前置过滤 Java (Spring Boot + Netty/Tomcat) 高并发、NIO 连接器
核心服务 Java (Spring Boot 3.x + 响应式/WebFlux) 高并发、生态成熟、企业级
缓存 Redis Cluster (6.x+) + Jedis/Lettuce Lettuce NIO 驱动、Lua 原子操作、集群扩展
消息队列 Kafka (2.8+) / RocketMQ 高吞吐、持久化、分区并行消费
数据库 MySQL 8.0 (InnoDB) + TiDB MySQL 成熟可靠 + TiDB 分布式强一致
ORM MyBatis-Plus + Druid 连接池 灵活 SQL、连接池监控
服务治理 Sentinel + Kubernetes + Nacos 熔断限流 + 配置中心 + 容器化弹性伸缩
监控 Prometheus + Grafana + ELK Micrometer 指标 + 可视化 + 日志聚合

三、流量接入层(网关设计)

流量接入层是秒杀系统的大门,它的设计质量直接决定了整个系统能否承受住第一波冲击。这一层的核心思想是「尽可能在边缘多拦截」,即让无效流量离系统核心越远越好。

为什么要在边缘拦截?因为每一层的处理成本是不等的。CDN 拦截一个请求的成本大约是 0.001 毫秒(静态文件直接返回),而让一个请求穿透到应用服务、访问 Redis、然后被限流拒绝,成本大约是 5~10 毫秒(一次完整的网络往返 + 逻辑处理)。当入口是 100W QPS 时,后者要多消耗 5000~10000 秒·毫秒的 CPU 时间,相当于额外 50~100 个 CPU 核心的持续负载。这个差异在秒杀场景下就是「系统雪崩」和「平稳运行」的区别。

因此我们在接入层投入了大量心思:CDN 缓存页面减少回源,LVS 用 DR 模式实现四层高性能转发,Nginx 用七层能力做精细路由,OpenResty 用 Lua 脚本在网关内部完成限流而不必转发到后端服务。每一层的设计都遵循同一个原则:用最低的成本、在最浅的层次拦截最多的无效流量。

3.1 CDN 层优化

设计思路:把 90% 的静态甚至准静态请求挡在 CDN 层,只让真正的秒杀“点击”请求穿透进来。

3.1.1 CDN 缓存策略

  • 活动页面 HTML:缓存时间设为秒杀开始前 30 分钟生成、TTL 60 秒(短缓存)
  • 商品图片 / 静态资源:CDN 长期缓存,文件名带 hash
  • 倒计时接口:返回页面状态的 API 设置 CDN 缓存 TTL=1s+ stale-while-revalidate

3.1.2 动态加速

使用 CDN 的动态加速链路(如阿里云 DCDN),减少回源延迟。秒杀页面使用 SSR(服务端渲染)+ CDN 边缘缓存:

// CDN 边缘编译:使用 Cloudflare Workers / 阿里云边缘函数
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // 1. 从边缘 KV 读取活动状态(实时更新)
  const activityStatus = await EDGE_KV.get('seckill_status')
  
  // 2. 已结束/未开始 → 直接返回静态页面(缓存)
  if (activityStatus !== 'ACTIVE') {
    return serveStaticPage(activityStatus)
  }
  
  // 3. 活动中 → 动态生成页面
  return generateLivePage()
}

3.2 负载均衡层

                    ┌──────────┐
                    │   LVS    │  (DR/TUN 模式,四层负载)
                    │  (Keepalived)  │  (主备 HA)
                    └─────┬────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐   ┌──────────┐   ┌──────────┐
    │ Nginx-1  │   │ Nginx-2  │   │ Nginx-3  │  (七层负载)
    └──────────┘   └──────────┘   └──────────┘

LVS 配置要点

  • DR(Direct Routing)模式:LVS 只处理入站请求,响应直接由 Nginx 返回客户端,单机支持 200W+ 并发
  • Keepalived 实现高可用,VIP 漂移 < 3 秒
  • 四层健康检查(TCP 端口探测)

Nginx 配置要点

  • upstream 轮询/upstream hash 路由(保证同一用户 hash 到同一台网关)
  • 开启 keepalive 连接复用(与后端服务)
  • worker_cpu_affinity 绑核
  • 开启 open_file_cache 减少文件 IO

3.3 API 网关层(OpenResty / APISIX)

API 网关是流量的第一道核心防线,承担限流、鉴权、路由等职责。

3.3.1 网关限流设计

采用多级限流策略,层层递进:

┌─────────────────────────────────────────────────┐
│              网关限流策略(从小到大)               │
│                                                 │
│  1. 全局限流:单机 10W QPS,集群 100W QPS         │
│  2. 用户维度:单用户 5 次/秒                       │
│  3. 活动维度:单活动 50W QPS                       │
│  4. 接口维度:单接口 5W QPS                        │
│  5. IP 维度:单 IP 20 次/秒                        │
│                                                 │
│  超出限流的请求直接返回 429 Too Many Requests     │
└─────────────────────────────────────────────────┘

OpenResty Lua 限流实现

-- nginx.conf 中引入限流 Lua 模块
location /api/seckill/ {
    access_by_lua_block {
        local limit_req = require "resty.limit.req"
        
        -- 全局漏桶限流:1000 req/s per worker,桶大小 2000
        local lim, err = limit_req.new("my_limit_req_store", 1000, 2000)
        
        -- 获取客户端 IP 作为 key
        local key = ngx.var.realip_remote_addr
        local delay, err = lim:incoming(key, true)
        
        if not delay then
            if err == "rejected" then
                -- 超出限流,返回 429
                ngx.status = 429
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"code":429,"message":"请求过于频繁,请稍后再试"}')
                ngx.exit(429)
            end
        end
        
        -- 如果有延迟需要等待(平滑处理突发流量)
        if delay >= 0.001 then
            ngx.sleep(delay)
        end
    }
    
    # 请求通过限流,转发后端
    proxy_pass http://seckill_backend;
}

3.3.2 动态令牌桶设计

秒杀场景更精确地控制流量漏斗,采用动态令牌发放

-- 动态令牌服务(Redis 预生成 + Lua 原子判断)
local redis = require "resty.redisseckill_token"

-- 每个用户进入秒杀页面前,先获取秒杀令牌
-- 令牌总量 = 商品库存 × 放大系数(如 10 倍)
-- 这样将大部分流量在"获取令牌"阶段就过滤掉

function acquire_token(user_id, activity_id)
    local token_key = "seckill:token:" .. activity_id
    local user_token_key = "seckill:user_token:" .. activity_id .. ":" .. user_id
    
    -- 检查用户是否已获取令牌(限领一个)
    local exists = redis:exists(user_token_key)
    if exists == 1 then
        return redis:get(user_token_key)
    end
    
    -- 尝试从令牌池获取令牌(原子递减)
    -- 令牌池预加载:stock * 10
    local token = redis:eval([[
        local pool_key = KEYS[1]
        local user_key = KEYS[2]
        local current = tonumber(redis.call('get', pool_key) or '0')
        if current > 0 then
            redis.call('decr', pool_key)
            local token = redis.call('incr', 'seckill:token_seq:' .. KEYS[3])
            redis.call('setex', user_key, 600, token)  -- 令牌有效期 600s
            return token
        end
        return '-1'
    ]], 3, token_key, user_token_key, activity_id)
    
    return token
end

3.3.3 请求过滤与清洗

-- 请求合法性过滤
location /api/seckill/do {
    access_by_lua_block {
        local args = ngx.req.get_uri_args()
        
        -- 1. 必填参数校验
        if not args.activity_id or not args.token or not args.timestamp then
            ngx.status = 400
            ngx.say('{"code":400,"message":"参数错误"}')
            ngx.exit(400)
        end
        
        -- 2. 时间戳校验(防重放,±5 分钟有效)
        local ts = tonumber(args.timestamp)
        local now = ngx.now() * 1000
        if math.abs(now - ts) > 300000 then
            ngx.status = 400
            ngx.say('{"code":400,"message":"请求时间戳无效"}')
            ngx.exit(400)
        end
        
        -- 3. 签名校验(HMAC-SHA256)
        local sign = ngx.req.get_headers()["X-Signature"]
        local expected_sign = ngx.hmac_sha1(SECRET_KEY, args.activity_id .. args.token .. args.timestamp)
        if sign ~= expected_sign then
            ngx.status = 403
            ngx.say('{"code":403,"message":"签名无效"}')
            ngx.exit(403)
        end
        
        -- 4. 黑名单 IP / 用户
        local blacklisted = redis:sismember("seckill:blacklist:ip", ngx.var.remote_addr)
        if blacklisted == 1 then
            ngx.status = 403
            ngx.exit(403)
        end
    }
    
    proxy_pass http://seckill_backend;
}

3.4 页面静态化 + 边缘推流

核心思路:秒杀页面在活动开始前全部静态化推送到 CDN 边缘,页面倒计时结束后通过 WebSocket/SSE 推送"开始"信号。

┌──────────────────────────────────────┐
│           客户端静态页面               │
│                                      │
│  1. 活动开始前 5 分钟:展示倒计时页面   │
│  2. 倒计时结束后:通过 SSE 接收信号     │
│  3. 收到 "START" 信号:激活抢购按钮      │
│  4. 用户点击按钮:发送秒杀请求          │
│                                      │
│  关键:将「用户看到页面」               │
│      与「请求到达服务器」在时间上错开     │
│      避免流量巨浪在同一瞬间涌入            │
└──────────────────────────────────────┘

四、核心业务层

核心业务层是秒杀系统的心脏,也是整个架构中最需要精心设计的一层。前几层解决的是「如何让系统不宕机」,而这一层解决的是「如何保证正确性的前提下处理尽可能多的请求」。

为什么这一层最关键?因为这里涉及两个核心难题的原子性解决:第一,库存扣减不能超卖。如果 Redis 先 GET 库存再 DECR,在这两个操作之间,其他请求可能也 GET 到了同样的库存值,全部进入扣减逻辑,最终导致库存变成负数。第二,用户去重和扣减必须原子。如果你先判断用户没有买过、然后再扣库存、最后记录用户已买,这之间的时间窗口内同一个用户的并发请求可能同时通过去重检查。

解决这两个问题的关键就是 Redis + Lua 脚本。Redis 是单线程模型,Lua 脚本在执行期间不会被其他命令打断。这意味着我们把「查库存 → 判断是否买过 → 扣减 → 记录用户」这四步操作写在一个 Lua 脚本里,就可以保证它们要么全部执行、要么全部不执行,不存在中间状态被其他请求看到的情况。这就是为什么秒杀系统中 Redis Lua 脚本是黄金标准。

另外,这一层的设计还有一个重要考量:尽可能「无状态化」。为什么?因为无状态意味着可以水平扩展——你可以简单地增加节点数来提升处理能力,而不需要担心状态同步的问题。所以我们将所有状态(库存、活动信息、用户标记)都存储在 Redis 中,Java 服务本身不维护任何业务状态。

4.1 服务分层

diagram (2).svg

┌─────────────────────────────────────────────────────────┐
│                   秒杀核心服务层                          │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │              接入层 (Gateway Service)             │   │
│  │  接收请求 → 参数解析 → 路由分发                    │   │
│  └─────────────────────┬───────────────────────────┘   │
│                         │                               │
│  ┌─────────────────────▼───────────────────────────┐   │
│  │           业务逻辑层 (Seckill Service)            │   │
│  │  活动校验 → 库存预扣 → Token 验证 → 结果生成      │   │
│  └─────────────────────┬───────────────────────────┘   │
│                         │                               │
│  ┌─────────────────────▼───────────────────────────┐   │
│  │           基础设施层 (Infrastructure)             │   │
│  │  Redis 客户端 │ MQ 生产者 │ DB 连接池 │ 服务注册    │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

4.2 核心秒杀接口设计

4.2.1 秒杀执行流程

用户请求
  │
  ▼
[1. 活动状态校验] ── 是否活动进行中?
  │                    否 → 返回"活动未开始/已结束"
  ▼
[2. Token 校验] ── 令牌是否有效?
  │                否 → 返回"未获取购买资格"
  ▼
[3. 用户去重] ── 该用户是否已抢购?
  │              是 → 返回"每人限购一次"
  ▼
[4. 库存预扣] ── Redis Lua 原子扣减
  │              库存不足 → 返回"已售罄"
  ▼
[5. 生成预订单号] ── 返回 order_token
  │
  ▼
[6. 发送 MQ 消息] ── 异步创建真实订单
  │
  ▼
返回用户:抢购成功,正在处理订单

4.2.2 Java 核心代码实现

// SeckillController.java - 秒杀核心服务
package com.seckill.controller;

import com.seckill.dto.SeckillRequest;
import com.seckill.dto.SeckillResponse;
import com.seckill.service.SeckillService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/seckill")
@RequiredArgsConstructor
public class SeckillController {

    private final SeckillService seckillService;

    /**
     * 执行秒杀
     */
    @PostMapping("/execute")
    public SeckillResponse executeSeckill(@RequestBody SeckillRequest request) {
        return seckillService.executeSeckill(request);
    }
}
// SeckillRequest.java - 秒杀请求入参
package com.seckill.dto;

import lombok.Data;

@Data
public class SeckillRequest {
    private String activityId;
    private Long userId;
    private String token;
    private String itemId;
}
// SeckillResponse.java - 秒杀响应
package com.seckill.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillResponse {
    private int code;
    private String message;
    private String orderToken;

    public static SeckillResponse ok(String message, String orderToken) {
        return new SeckillResponse(0, message, orderToken);
    }

    public static SeckillResponse fail(int code, String message) {
        return new SeckillResponse(code, message, null);
    }
}
// SeckillService.java - 秒杀核心业务逻辑
package com.seckill.service;

import com.seckill.dto.SeckillRequest;
import com.seckill.dto.SeckillResponse;
import com.seckill.dto.SeckillOrderMessage;
import com.seckill.mq.SeckillMQProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillService {

    private final SeckillCacheService cacheService;
    private final SeckillMQProducer mqProducer;

    /**
     * 执行秒杀核心逻辑
     */
    public SeckillResponse executeSeckill(SeckillRequest req) {

        // Step 1: 活动状态校验(本地缓存 + Redis)
        if (!cacheService.checkActivityStatus(req.getActivityId())) {
            return SeckillResponse.fail(4001, "活动未开始或已结束");
        }

        // Step 2: Token 校验
        if (!cacheService.validateToken(req)) {
            return SeckillResponse.fail(4002, "购买资格已失效,请重新获取");
        }

        // Step 3: Redis Lua 脚本原子扣减库存 + 用户去重
        // 这是整个系统最核心的环节
        String result;
        try {
            result = cacheService.deductStock(req);
        } catch (Exception e) {
            log.error("库存扣减异常", e);
            return SeckillResponse.fail(4003, "系统繁忙,请重试");
        }

        if (!"1".equals(result)) {
            return SeckillResponse.fail(4004, "商品已售罄");
        }

        // Step 4: 生成预订单 Token
        String orderToken = generateOrderToken(req);

        // Step 5: 异步发送到 MQ(非阻塞,异常不影响主流程)
        try {
            mqProducer.sendOrderMessage(SeckillOrderMessage.builder()
                    .activityId(req.getActivityId())
                    .userId(req.getUserId())
                    .itemId(req.getItemId())
                    .orderToken(orderToken)
                    .timestamp(System.currentTimeMillis())
                    .build());
        } catch (Exception e) {
            log.error("MQ发送失败,将由对账补偿: {}", orderToken, e);
            // MQ发送失败不阻断流程,后续由对账 Job 补偿
        }

        return SeckillResponse.ok("抢购成功,正在生成订单", orderToken);
    }

    private String generateOrderToken(SeckillRequest req) {
        return req.getActivityId() + "_" + req.getUserId() + "_" + System.nanoTime();
    }
}

4.2.3 Redis Lua 原子库存扣减

这是秒杀系统最核心的技术点。使用 Redis 的 Lua 脚本保证「查库存 → 扣减 → 记录用户」三步操作的原子性:

-- seckill_deduct.lua
-- KEYS[1]: 库存计数 key, 如 "seckill:stock:1001"
-- KEYS[2]: 用户去重 set key, 如 "seckill:buyers:1001"
-- ARGV[1]: 用户 ID
-- ARGV[2]: 预订单号

local stock_key = KEYS[1]
local buyer_key = KEYS[2]
local user_id = ARGV[1]
local order_token = ARGV[2]

-- 1. 用户去重:检查该用户是否已抢购
local already_bought = redis.call('sismember', buyer_key, user_id)
if already_bought == 1 then
    return '-1'  -- 已抢购过
end

-- 2. 查询当前库存
local stock = tonumber(redis.call('get', stock_key))
if stock == nil then
    return '-2'  -- 库存 key 不存在(活动未加载)
end

if stock <= 0 then
    return '0'   -- 库存已售罄
end

-- 3. 原子扣减库存
local new_stock = redis.call('decr', stock_key)
if new_stock < 0 then
    -- 因并发刚好超扣,回滚
    redis.call('incr', stock_key)
    return '0'   -- 实际已无库存
end

-- 4. 记录抢购用户(去重)
redis.call('sadd', buyer_key, user_id)

-- 5. 记录用户 → 预订单号映射
redis.call('setex', 'seckill:order:' .. user_id .. ':' .. order_token, 600, 'pending')

-- 6. 可选:将抢购成功用户放入待处理队列
redis.call('lpush', 'seckill:order_queue:1001', 
    user_id .. ':' .. order_token)

return '1'  -- 扣减成功
// SeckillCacheService.java - Redis 缓存操作
package com.seckill.service;

import com.seckill.dto.SeckillRequest;
import com.seckill.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Collections;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillCacheService {

    private final StringRedisTemplate redisTemplate;

    /**
     * 秒杀库存扣减 Lua 脚本(原子操作:查用户去重 + 查库存 + 扣减 + 记录用户)
     */
    private static final String STOCK_DEDUCT_SCRIPT = 
        "local stock_key = KEYS[1]\n" +
        "local buyer_key = KEYS[2]\n" +
        "local user_id = ARGV[1]\n" +
        "local already_bought = redis.call('sismember', buyer_key, user_id)\n" +
        "if already_bought == 1 then return '-1' end\n" +
        "local stock = tonumber(redis.call('get', stock_key))\n" +
        "if stock == nil then return '-2' end\n" +
        "if stock <= 0 then return '0' end\n" +
        "local new_stock = redis.call('decr', stock_key)\n" +
        "if new_stock < 0 then\n" +
        "    redis.call('incr', stock_key)\n" +
        "    return '0'\n" +
        "end\n" +
        "redis.call('sadd', buyer_key, user_id)\n" +
        "return '1'";

    private final DefaultRedisScript<String> stockDeductScript;

    public SeckillCacheService() {
        this.stockDeductScript = new DefaultRedisScript<>();
        this.stockDeductScript.setScriptText(STOCK_DEDUCT_SCRIPT);
        this.stockDeductScript.setResultType(String.class);
    }

    /**
     * 执行库存扣减
     * @return "1" 成功, "0" 库存不足, "-1" 已抢购过, "-2" 活动未加载
     */
    public String deductStock(SeckillRequest req) {
        String stockKey = "seckill:stock:" + req.getActivityId();
        String buyerKey = "seckill:buyers:" + req.getActivityId();

        return redisTemplate.execute(
            stockDeductScript,
            Arrays.asList(stockKey, buyerKey),
            String.valueOf(req.getUserId())
        );
    }

    /**
     * 活动状态校验
     */
    public boolean checkActivityStatus(String activityId) {
        String status = redisTemplate.opsForValue().get(
            "seckill:activity:" + activityId + ":status");
        return "ACTIVE".equals(status);
    }

    /**
     * Token 校验
     */
    public boolean validateToken(SeckillRequest req) {
        String userTokenKey = "seckill:user_token:" + req.getActivityId() 
            + ":" + req.getUserId();
        String token = redisTemplate.opsForValue().get(userTokenKey);
        return req.getToken() != null && req.getToken().equals(token);
    }
}

4.3 服务降级与熔断

// SentinelConfig.java - Sentinel 熔断降级配置
package com.seckill.config;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class SentinelConfig {

    @PostConstruct
    public void initRules() {
        // 1. 流控规则:秒杀入口限流 100000 QPS,排队超时 500ms
        List<FlowRule> flowRules = new ArrayList<>();
        FlowRule seckillRule = new FlowRule();
        seckillRule.setResource("seckill_execute");      // 资源名
        seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS 维度
        seckillRule.setCount(100000);                      // 阈值 10W
        seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); // 排队限流
        seckillRule.setMaxQueueingTimeoutMs(500);          // 最大排队 500ms
        flowRules.add(seckillRule);
        FlowRuleManager.loadRules(flowRules);

        // 2. 熔断规则:DB 写入链路,错误率 50% 触发熔断,恢复时间 30s
        List<DegradeRule> degradeRules = new ArrayList<>();
        DegradeRule dbWriteRule = new DegradeRule();
        dbWriteRule.setResource("seckill_db_write");
        dbWriteRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); // 错误率策略
        dbWriteRule.setCount(0.5);    // 错误率阈值 50%
        dbWriteRule.setStatIntervalMs(10000);  // 统计窗口 10s
        dbWriteRule.setMinRequestAmount(100);  // 最小请求量
        dbWriteRule.setTimeWindow(30);         // 熔断时长 30s
        degradeRules.add(dbWriteRule);
        DegradeRuleManager.loadRules(degradeRules);
    }
}
// SeckillService.java(续)- 降级逻辑
package com.seckill.service;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.seckill.dto.SeckillRequest;
import com.seckill.dto.SeckillResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillServiceDegradation {

    private final SeckillService seckillService;

    /**
     * 带降级保护的秒杀执行入口
     */
    public SeckillResponse executeWithDegradation(SeckillRequest req) {
        Entry entry = null;
        try {
            // 尝试通过 Sentinel 限流入口
            entry = SphU.entry("seckill_execute");
            
            // 正常执行秒杀逻辑
            return seckillService.executeSeckill(req);
            
        } catch (BlockException e) {
            // 被限流 → 降级处理
            log.warn("秒杀请求被限流: userId={}, activityId={}", 
                     req.getUserId(), req.getActivityId());
            
            // 降级策略 1:返回友好提示(保留用户体验)
            if ("friendly".equals(getDegradationMode())) {
                return SeckillResponse.fail(429, "当前参与人数较多,请稍候重试");
            }
            
            // 降级策略 2:本地概率过滤(拒绝部分请求)
            if (localLotteryFilter()) {
                return SeckillResponse.fail(503, "系统繁忙,请稍后");
            }
            
            return SeckillResponse.fail(429, "当前参与人数较多,请稍候重试");
            
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }

    /**
     * 本地概率过滤器:以一定概率拒绝请求,平滑流量
     */
    private boolean localLotteryFilter() {
        // 拒绝 80% 的请求
        return Math.random() < 0.8;
    }

    private String getDegradationMode() {
        return "friendly"; // 实际可从 Nacos 配置中心读取
    }
}
Maven 依赖 (pom.xml):
    
    <!-- Sentinel 核心 + 集成 -->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-core</artifactId>
        <version>1.8.6</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-annotation-aspectj</artifactId>
        <version>1.8.6</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- Nacos 配置中心 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

五、缓存层设计

缓存层是秒杀系统中技术含量最高的一层,也是决定系统能否突破百万 QPS 的关键瓶颈。为什么这么说?因为数据库的写入能力(单机大约 5000 TPS)和缓存层的读取能力(单机大约 10W OPS)之间存在着 20 倍的性能差距。如果能让交易在缓存层就完成裁决,只把最终成功的订单写入数据库,就能极大地缓解下游压力。

缓存层设计中最突出的问题是「热点 Key」。在秒杀场景中,所有请求操作的都是同一批商品——如果是单件商品秒杀,就意味着所有 100W QPS 的流量都打在同一个 Redis Key 上。即使 Redis 单节点有 10W OPS 的能力,面对 100W 的集中访问也会被瞬间打爆。这个问题在技术圈被称为「Hot Key 问题」,它无法通过简单的水平扩展来解决——因为 Cluster 模式下同一个 Slot 始终在同一台节点上。

为了解决这个问题,我们采用了一种「本地缓存过滤 + 分桶分散」的组合策略。首先在应用层每台机器上维护一个本地 AtomicLong 计数器(从 Redis 一次性读取库存),请求到达时先在本地递减,归零后直接返回售罄,不再访问 Redis。这样可以在本地拦截 90% 的流量。其次,对于剩余的流量,我们将库存拆分为多个桶(Buckets),不同用户的请求根据用户 ID 的哈希值分流到不同的桶上,将单 Key 压力分散为 10 个 Key。

除了热点 Key,缓存层还要解决三个经典问题:缓存穿透(查询不存在的 Key,绕过缓存打 DB)、缓存击穿(热点 Key 过期瞬间大量并发重建)、缓存雪崩(大量 Key 同时过期)。我们在后文中都给出了具体的解决方案。

5.1 Redis 集群部署架构

┌──────────────────────────────────────────────────────────────┐
│                    Redis Cluster (6.x+)                       │
│                                                              │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐                │
│   │ Master-1│    │ Master-2│    │ Master-3│                │
│   │ Slot 0  │    │ Slot 1  │    │ Slot 2  │                │
│   │  -5460  │    │5461-10922│   │10923-16383│              │
│   └────┬────┘    └────┬────┘    └────┬────┘                │
│        │              │              │                       │
│   ┌────▼────┐    ┌────▼────┐    ┌────▼────┐                │
│   │ Replica1│    │ Replica2│    │ Replica3│                │
│   └─────────┘    └─────────┘    └─────────┘                │
│                                                              │
│   配置:                                                      │
│   - 每节点 16GB 内存(库存计数器只占极少内存)                    │
│   - Cluster-Require-Full-Coverage: yes                       │
│   - Maxmemory-policy: noeviction                             │
│   - 开启 AOF 持久化(appendonly yes, everysec)保库存不丢    │
│   - 开启 Redis 客户端读写分离(读走 Slave)                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

5.2 缓存 Key 设计

┌─────────────────────────────────────────────────────────────┐
│                      Redis Key 设计规范                      │
│                                                             │
│  1. 库存计数:                                                │
│     seckill:stock:{activity_id}                              │
│     → String 类型,存储整数库存数量                              │
│     → 活动开始前从 DB 加载                                    │
│     → 例:set seckill:stock:1001 500                         │
│                                                             │
│  2. 用户去重:                                                │
│     seckill:buyers:{activity_id}                             │
│     → Set 类型,存储已抢购成功的用户 ID                         │
│     → 用于 Lua 脚本中判断重复抢购                               │
│     → 活动结束后归档到 DB 并清除                                │
│                                                             │
│  3. 活动信息缓存:                                             │
│     seckill:activity:{activity_id}                           │
│     → Hash 类型,存储活动元数据                                 │
│     → 包含:startTime, endTime, status, itemCount, price      │
│     → 设置 TTL 避免过期                                       │
│                                                             │
│  4. 令牌池:                                                  │
│     seckill:token:{activity_id}                             │
│     → String 类型,存储剩余令牌数量                             │
│     → 预加载值 = itemCount * 10                               │
│     → 每用户获取后递减                                        │
│                                                             │
│  5. 用户资格:                                                │
│     seckill:user_token:{activity_id}:{user_id}               │
│     → String 类型,值为用户持有的 token                        │
│     → TTL = 600s(秒杀进行中有效)                              │
│                                                             │
│  6. 结果通知(Pub/Sub):                                     │
│     seckill:result:{user_id}:{activity_id}                   │
│     → 用于长轮询/推送秒杀结果                                   │
│                                                             │
│  7. 订单创建状态:                                             │
│     seckill:order_status:{order_token}                       │
│     → String 类型,pending / success / failed                │
│     → 客户端轮询此 key 获取最终结果                             │
│                                                             │
│  8. 预热标记:                                                │
│     seckill:warmed:{activity_id}                              │
│     → String,标记缓存预热是否完成                               │
│     → 防止缓存击穿                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.3 缓存预热

活动开始前,必须将热点数据提前加载到 Redis:

// SeckillWarmupService.java - 缓存预热服务
package com.seckill.service;

import com.seckill.entity.SeckillActivity;
import com.seckill.mapper.SeckillActivityMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillWarmupService {

    private final StringRedisTemplate redisTemplate;
    private final SeckillActivityMapper activityMapper;

    /**
     * 活动开始前 30 分钟触发预热
     * 实际部署中也可由运营平台手动触发
     */
    @Scheduled(cron = "0 30 * * * ?") // 每小时第 30 分钟(示例)
    public void warmupUpcomingActivities() {
        // 查询即将开始的活动
        List<String> upcomingIds = activityMapper.selectUpcomingActivityIds();
        for (String activityId : upcomingIds) {
            try {
                warmupSingleActivity(activityId);
            } catch (Exception e) {
                log.error("活动预热失败: activityId={}", activityId, e);
                // 失败不阻断其他活动
            }
        }
    }

    /**
     * 预热单个活动的全部缓存数据
     */
    public void warmupSingleActivity(String activityId) {
        // 1. 检查是否已预热(防重复预热标志)
        String warmedKey = "seckill:warmed:" + activityId;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(warmedKey))) {
            log.info("活动已预热,跳过: {}", activityId);
            return;
        }

        // 2. 从 DB 加载活动信息
        SeckillActivity activity = activityMapper.selectByActivityId(activityId);
        if (activity == null) {
            throw new RuntimeException("活动不存在: " + activityId);
        }

        // 3. 使用 Redis Pipeline 批量写入(减少网络往返)
        redisTemplate.executePipelined(connection -> {
            // 库存
            connection.stringCommands().set(
                ("seckill:stock:" + activityId).getBytes(),
                String.valueOf(activity.getItemCount()).getBytes()
            );
            connection.keyCommands().expire(
                ("seckill:stock:" + activityId).getBytes(),
                Duration.ofHours(24).getSeconds()
            );

            // 活动信息(Hash)
            Map<String, String> activityInfo = new HashMap<>();
            activityInfo.put("name", activity.getName());
            activityInfo.put("startTime", String.valueOf(activity.getStartTime().getTime()));
            activityInfo.put("endTime", String.valueOf(activity.getEndTime().getTime()));
            activityInfo.put("status", "UPCOMING");
            activityInfo.put("price", activity.getPrice().toString());
            connection.hashCommands().hMSet(
                ("seckill:activity:" + activityId).getBytes(),
                activityInfo.entrySet().stream()
                    .collect(Collectors.toMap(
                        e -> e.getKey().getBytes(),
                        e -> e.getValue().getBytes()
                    ))
            );
            connection.keyCommands().expire(
                ("seckill:activity:" + activityId).getBytes(),
                Duration.ofHours(24).getSeconds()
            );

            // Token 池(放大 10 倍)
            int tokenCount = activity.getItemCount() * 10;
            connection.stringCommands().set(
                ("seckill:token:" + activityId).getBytes(),
                String.valueOf(tokenCount).getBytes()
            );
            connection.keyCommands().expire(
                ("seckill:token:" + activityId).getBytes(),
                Duration.ofHours(24).getSeconds()
            );

            // Token 序列号初始化
            connection.stringCommands().set(
                ("seckill:token_seq:" + activityId).getBytes(),
                "0".getBytes()
            );
            connection.keyCommands().expire(
                ("seckill:token_seq:" + activityId).getBytes(),
                Duration.ofHours(24).getSeconds()
            );

            return null;
        });

        // 4. 设置预热完成标志
        redisTemplate.opsForValue().set(warmedKey, "1", Duration.ofHours(24));

        log.info("活动预热完成: activityId={}, stock={}", activityId, activity.getItemCount());
    }

    /**
     * 定时刷新活动状态(当活动开始时,将状态从 UPCOMING 改为 ACTIVE)
     */
    @Scheduled(fixedRate = 5000) // 每 5 秒检查一次
    public void refreshActivityStatus() {
        List<SeckillActivity> activities = activityMapper.selectActivitiesNeedingActivation();
        for (SeckillActivity activity : activities) {
            String now = String.valueOf(System.currentTimeMillis());
            redisTemplate.opsForHash().put(
                "seckill:activity:" + activity.getActivityId(),
                "status", "ACTIVE"
            );
            log.info("活动状态已激活: {}", activity.getActivityId());
        }
    }
}

5.4 缓存热点问题优化

秒杀是典型的单 Key 热点问题:所有请求都在操作同一个库存 Key。解决方案:

5.4.1 本地缓存 + 分段锁

方案一:本地标记快速过滤

秒杀开始时,各节点从 Redis 一次性读取库存值到本地内存。
请求到达时先检查本地库存变量:
  - 本地库存 <= 0 → 直接返回"已售罄"(不再访问 Redis)
  - 本地库存 > 0 → 递减本地库存,再到 Redis 扣减
  
这样可以拦截 90%+ 的流量到本地,不在 Redis 上竞争。
// LocalStockFilter.java - 本地库存过滤器
package com.seckill.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.atomic.AtomicLong;

/**
 * 本地内存库存过滤器(解决单 Key 热点问题)
 * 
 * 原理:秒杀开始时,一次性从 Redis 读取库存到本地 AtomicLong
 * 请求到达时先检查本地变量,<= 0 直接返回售罄,不走 Redis
 * 这样可以拦截 90%+ 的流量,避免大量请求在 Redis 上竞争
 */
@Slf4j
@Component
public class LocalStockFilter {

    private final AtomicLong localStock = new AtomicLong(0);
    private final StringRedisTemplate redisTemplate;

    public LocalStockFilter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 启动仪式:从 Redis 加载库存到本地
     */
    public void initStock(String activityId) {
        String stockStr = redisTemplate.opsForValue().get("seckill:stock:" + activityId);
        long stock = stockStr != null ? Long.parseLong(stockStr) : 0;
        localStock.set(stock);
        log.info("本地库存过滤器初始化: activityId={}, stock={}", activityId, stock);
    }

    /**
     * 尝试消费一个本地库存
     * @return true 表示本地有库存(可继续走 Redis),false 表示本地已空(直接拒绝)
     */
    public boolean tryConsume() {
        long current;
        do {
            current = localStock.get();
            if (current <= 0) {
                return false;  // 本地库存耗尽,直接拦截
            }
        } while (!localStock.compareAndSet(current, current - 1));
        return true;  // CAS 成功,扣减了本地库存
    }

    /**
     * 回滚本地库存(Redis 扣减失败时补偿回来)
     */
    public void rollback() {
        localStock.incrementAndGet();
    }
}
// SeckillService.java(续)- 使用本地过滤器
package com.seckill.service;

import com.seckill.filter.LocalStockFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillServiceWithFilter {

    private final LocalStockFilter localStockFilter;
    private final SeckillCacheService cacheService;
    private final SeckillMQProducer mqProducer;

    public SeckillResponse executeWithLocalFilter(SeckillRequest req) {

        // Step 1:本地过滤器快速拦截(无锁 CAS,纳秒级)
        if (!localStockFilter.tryConsume()) {
            return SeckillResponse.fail(4004, "商品已售罄");
        }

        // Step 2:活动状态 + Token 校验
        if (!cacheService.checkActivityStatus(req.getActivityId())) {
            localStockFilter.rollback();  // 回滚本地库存
            return SeckillResponse.fail(4001, "活动未开始或已结束");
        }

        if (!cacheService.validateToken(req)) {
            localStockFilter.rollback();
            return SeckillResponse.fail(4002, "购买资格已失效,请重新获取");
        }

        // Step 3:Redis Lua 原子扣减
        String result;
        try {
            result = cacheService.deductStock(req);
        } catch (Exception e) {
            localStockFilter.rollback();
            return SeckillResponse.fail(4003, "系统繁忙,请重试");
        }

        if (!"1".equals(result)) {
            localStockFilter.rollback();  // Redis 扣减失败,回滚本地库存
            return SeckillResponse.fail(4004, "商品已售罄");
        }

        // Step 4:生成订单 Token + 发 MQ
        String orderToken = req.getActivityId() + "_" + req.getUserId() + "_" + System.nanoTime();
        mqProducer.sendOrderMessage(SeckillOrderMessage.builder()
                .activityId(req.getActivityId())
                .userId(req.getUserId())
                .itemId(req.getItemId())
                .orderToken(orderToken)
                .timestamp(System.currentTimeMillis())
                .build());

        return SeckillResponse.ok("抢购成功,正在生成订单", orderToken);
    }
}

5.4.2 库存分桶方案

方案二:库存分段分散热点

将库存 (如 1000 件) 分成 10 个分桶,每个桶 100 件:

  seckill:stock:1001:bucket:0  → 100
  seckill:stock:1001:bucket:1  → 100
  ...
  seckill:stock:1001:bucket:9  → 100

请求到达时:
1. 随机选择一个桶(或用 hash 分散)
2. 先尝试在该桶扣减
3. 如果该桶库存不足,尝试其他桶
4. 所有桶都无库存时才返回售罄

优点:将单 Key 热点分散到 10 个 Key,理论吞吐提升 10 倍
private static final int BUCKET_COUNT = 10;

/**
 * 分桶库存扣减:将单 Key 热点分散到多个桶
 */
public String deductStockDistributed(SeckillRequest req) {
    // 基于 userId hash 选择桶,保证同一用户始终走同一个桶
    int bucket = (int) (req.getUserId() % BUCKET_COUNT);

    // 先尝试主桶
    String result = deductFromBucket(req, bucket);
    if ("1".equals(result)) {
        return result;
    }

    // 主桶无库存,尝试其他桶
    for (int i = 1; i < BUCKET_COUNT; i++) {
        int altBucket = (bucket + i) % BUCKET_COUNT;
        result = deductFromBucket(req, altBucket);
        if ("1".equals(result)) {
            return result;
        }
    }

    return "0"; // 全部桶都无库存
}

private String deductFromBucket(SeckillRequest req, int bucket) {
    String stockKey = "seckill:stock:" + req.getActivityId() + ":bucket:" + bucket;
    String buyerKey = "seckill:buyers:" + req.getActivityId();

    // 同样使用 Lua 脚本(复用 DeductStockScript)
    return redisTemplate.execute(
        stockDeductScript,
        Arrays.asList(stockKey, buyerKey),
        String.valueOf(req.getUserId())
    );
}

5.5 缓存三大问题应对

5.5.1 缓存穿透

问题:查询不存在的 Key,绕过缓存打到 DB。

解决

  • 在 Redis 中使用 Bloom Filter 预先存储所有有效的活动 ID
  • 请求到达先过 Bloom Filter,如果直接判定为不存在则拒绝
// SeckillBloomFilter.java - BloomFilter 防缓存穿透
package com.seckill.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillBloomFilter {

    private final StringRedisTemplate redisTemplate;

    /**
     * 初始化 Bloom Filter(活动加载时调用一次)
     * 使用 RedisBloom 模块
     */
    public void initBloomFilter(List<String> activityIds) {
        for (String id : activityIds) {
            redisTemplate.execute((RedisCallback<Object>) connection ->
                connection.execute("BF.ADD", "seckill:bloom:activities".getBytes(), id.getBytes())
            );
        }
        log.info("BloomFilter 初始化完成,活动数: {}", activityIds.size());
    }

    /**
     * 请求校验:判断活动 ID 是否可能存在
     * @return true 可能存在(放行),false 一定不存在(直接拒绝)
     */
    public boolean mayExist(String activityId) {
        Long exists = redisTemplate.execute((RedisCallback<Long>) connection ->
            connection.execute("BF.EXISTS", "seckill:bloom:activities".getBytes(), activityId.getBytes())
        );
        return exists != null && exists == 1L;
    }
}

5.5.2 缓存击穿

问题:热点 Key 过期瞬间,大量请求同时涌入 DB。

解决

  • 秒杀活动 Key 设置 永不过期(通过后台定时刷新)
  • 预热完成后永不主动删除,活动结束时手动清除
  • 使用 互斥锁(SETNX)防止并发重建
    /**
     * 防缓存击穿:使用 SETNX 互斥锁保护 DB 回源
     */
    public SeckillActivity getActivityInfo(String activityId) {
        String cacheKey = "seckill:activity:" + activityId;

        // 1. 尝试从缓存读取
        Map<Object, Object> cached = redisTemplate.opsForHash().entries(cacheKey);
        if (cached != null && !cached.isEmpty()) {
            return parseActivity(cached);
        }

        // 2. 缓存未命中,尝试获取分布式锁(SETNX,TTL 5 秒)
        String lockKey = "lock:" + cacheKey;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(5));

        if (Boolean.TRUE.equals(locked)) {
            try {
                // 3. 获取到锁,从 DB 读取
                SeckillActivity activity = activityMapper.selectByActivityId(activityId);
                if (activity != null) {
                    // 4. 写入缓存
                    Map<String, String> info = new HashMap<>();
                    info.put("name", activity.getName());
                    info.put("startTime", String.valueOf(activity.getStartTime().getTime()));
                    info.put("endTime", String.valueOf(activity.getEndTime().getTime()));
                    info.put("status", "ACTIVE");
                    info.put("price", activity.getPrice().toString());
                    redisTemplate.opsForHash().putAll(cacheKey, info);
                    redisTemplate.expire(cacheKey, Duration.ofHours(24));
                }
                return activity;
            } finally {
                // 5. 释放锁
                redisTemplate.delete(lockKey);
            }
        } else {
            // 6. 未获取到锁,短暂等待后重试
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return getActivityInfo(activityId); // 递归重试
        }
    }

#### 5.5.3 缓存雪崩

**问题**:大量 Key 同时过期,请求全部打到 DB。

**解决**:
- 秒杀 Key 不设置 TTL(永不过期),通过后台定时补充刷新
- 必须设置过期时间的,添加随机值:TTL + random(0, 5min)

```java
// 如果业务上确实需要 TTL(如用户资格 Token),添加随机值分散过期压力
int ttl = 600 + new Random().nextInt(60);  // 600-660 秒,分散过期压力
redisTemplate.opsForValue().set(userTokenKey, token, ttl, TimeUnit.SECONDS);

六、消息队列(MQ)设计

消息队列在秒杀系统中扮演着「流量整形器」的角色。为什么需要消息队列?因为在前端(网关层、Redis 层)处理请求的速度远远快于数据库写入的速度。Redis 扣减库存只需要 1~2 毫秒,但数据库写入需要 5~10 毫秒(包含事务、索引更新、binlog 刷盘等)。如果不进行流量整形,在高并发下数据库会被瞬间打满。

消息队列的核心价值在于「削峰填谷」——前端可以快速地把所有成功的秒杀请求塞入 MQ(Kafka 单机写入可达 40W msg/s),然后后端的消费服务按照数据库的承受能力匀速消费。这就像在高压水塔和家用水管之间加了一个储水池:水管(前端)可以瞬间放空,水龙头(后端)按需要使用,水压(流量)始终保持平稳。

在秒杀场景中,消息队列的设计有几个特殊考量:

  1. 消息不能丢失:秒杀成功一条消息对应的是一份真实订单和一笔真实收入。任何消息丢失都意味着用户付了钱但我们没发货。因此使用 acks=all + 全副本确认 + 生产者重试。

  2. 消息不能重复消费:网络抖动和消费者重启会导致 Kafka 重复投递消息。需要在消费端保证幂等性(唯一键约束 + 状态检查)。

  3. 相同活动的消息需要有序:同一用户不能同时有两个秒杀操作并行执行(否则可能超卖)。通过 activityId + userId 作为 partition key 保证同一用户的消息在同一分区顺序消费。

6.1 选型分析

维度 Kafka RocketMQ
吞吐量 极高(单分区 40W msg/s) 高(单分区 10W msg/s)
延迟 ms 级 ms 级
事务消息 不原生支持 原生支持
延时消息 不原生支持 原生支持(18 个级别)
消息回溯 支持(offset 灵活) 支持
集群运维 中等(需配合 ZK/KRaft) 较简单

最终选择

  • 首选 Kafka:仅需实现高吞吐、可靠投递、顺序消费,Kafka 生态更成熟
  • 备选 RocketMQ:若需要事务消息、延时消息等高级特性

6.2 Kafka Topic 设计

┌──────────────────────────────────────────────────────────┐
│                    Kafka Topic 设计                       │
│                                                          │
│  Topic: seckill_orders                                   │
│  ┌──────────────────────────────────────────┐           │
│  │ Partitions: 64(按 activity_id 分桶)      │           │
│  │ Replication Factor: 3                     │           │
│  │ min.insync.replicas: 2                    │           │
│  │ Retention: 24 hours                       │           │
│  │ ACKS: -1 (全副本确认)                      │           │
│  │ Compression: lz4                          │           │
│  └──────────────────────────────────────────┘           │
│                                                          │
│  Topic: seckill_order_retry                              │
│  ┌──────────────────────────────────────────┐           │
│  │ 死信/重试 topic(消费失败的消息)            │           │
│  │ Partitions: 16                            │           │
│  └──────────────────────────────────────────┘           │
│                                                          │
│  Topic: seckill_stock_compensate                         │
│  ┌──────────────────────────────────────────┐           │
│  │ 库存回滚 topic(订单超时取消时回滚库存)      │           │
│  │ Partitions: 8                             │           │
│  └──────────────────────────────────────────┘           │
│                                                          │
│  Topic: seckill_audit_log                                │
│  ┌──────────────────────────────────────────┐           │
│  │ 审计日志 topic(用于对账、监控)              │           │
│  │ Partitions: 8                             │           │
│  │ Retention: 7 days                         │           │
│  └──────────────────────────────────────────┘           │
│                                                          │
└──────────────────────────────────────────────────────────┘

6.3 生产者设计(MQ Producer)

// SeckillMQProducer.java - MQ 生产者
package com.seckill.mq;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.seckill.dto.SeckillOrderMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillMQProducer {

    private static final String TOPIC_SECKILL_ORDERS = "seckill_orders";

    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;

    /**
     * 同步发送秒杀订单消息
     */
    public void sendOrderMessage(SeckillOrderMessage msg) {
        try {
            String data = objectMapper.writeValueAsString(msg);
            // Key 设计:activityId + userId 哈希决定分区
            // 保证同一用户的消息有序进入同一分区
            String key = msg.getActivityId() + "_" + msg.getUserId();

            CompletableFuture<SendResult<String, String>> future = 
                kafkaTemplate.send(TOPIC_SECKILL_ORDERS, key, data);

            // 同步等待确认(超时 3s)
            SendResult<String, String> result = future.get(3000, TimeUnit.MILLISECONDS);
            log.debug("MQ发送成功: partition={}, offset={}, orderToken={}",
                    result.getRecordMetadata().partition(),
                    result.getRecordMetadata().offset(),
                    msg.getOrderToken());

        } catch (Exception e) {
            log.error("MQ发送失败: orderToken={}", msg.getOrderToken(), e);
            // 失败不阻断流程,由对账 Job 补偿
            throw new RuntimeException("MQ发送失败", e);
        }
    }
}
// SeckillMQProducer.java(续)- 异步发送版本(更高吞吐)
@Slf4j
@Component
public class SeckillMQProducerAsync {

    private static final String TOPIC_SECKILL_ORDERS = "seckill_orders";

    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;
    private final MeterRegistry meterRegistry;

    /**
     * 异步发送秒杀订单消息(非阻塞,失败重试)
     */
    public CompletableFuture<Void> sendOrderMessageAsync(SeckillOrderMessage msg) {
        try {
            String data = objectMapper.writeValueAsString(msg);
            String key = msg.getActivityId() + "_" + msg.getUserId();

            CompletableFuture<SendResult<String, String>> future = 
                kafkaTemplate.send(TOPIC_SECKILL_ORDERS, key, data);

            return future.whenComplete((result, ex) -> {
                if (ex != null) {
                    // 发送失败处理
                    log.error("MQ异步发送失败: orderToken={}, error={}", 
                              msg.getOrderToken(), ex.getMessage());
                    // 重试 3 次
                    retrySendMessage(msg, 1);
                }
            }).thenApply(r -> null);

        } catch (JsonProcessingException e) {
            log.error("消息序列化失败: {}", msg.getOrderToken(), e);
            return CompletableFuture.failedFuture(e);
        }
    }

    /**
     * 重试发送消息
     */
    private void retrySendMessage(SeckillOrderMessage msg, int retryCount) {
        if (retryCount > 3) {
            log.warn("MQ重试次数耗尽,转入DLQ: {}", msg.getOrderToken());
            sendToDLQ(msg);
            return;
        }

        try {
            Thread.sleep(retryCount * 500L); // 指数退避:500ms, 1000ms, 1500ms
            String data = objectMapper.writeValueAsString(msg);
            kafkaTemplate.send("seckill_orders", 
                msg.getActivityId() + "_" + msg.getUserId(), data);
            log.info("MQ重试成功: retryCount={}, orderToken={}", retryCount, msg.getOrderToken());
        } catch (Exception e) {
            retrySendMessage(msg, retryCount + 1);
        }
    }
    // Kafka producer 配置 (application.yml):
    #
    # spring:
    #   kafka:
    #     bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092
    #     producer:
    #       acks: all                    # 全副本确认,保证消息不丢
    #       retries: 3                   # 生产端重试
    #       batch-size: 65536            # 64KB 批次,吞吐优化
    #       linger-ms: 5                # 等待 5ms 聚合批次
    #       buffer-memory: 33554432     # 32MB 缓冲区
    #       compression-type: lz4       # LZ4 压缩
    #       max-in-flight-requests-per-connection: 1  # 保证有序
    #       key-serializer: org.apache.kafka.common.serialization.StringSerializer
    #       value-serializer: org.apache.kafka.common.serialization.StringSerializer
}

6.4 消费者设计(MQ Consumer)

// SeckillOrderConsumer.java - 秒杀订单消费者
package com.seckill.mq;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.seckill.dto.SeckillOrderMessage;
import com.seckill.service.SeckillOrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillOrderConsumer {

    private final SeckillOrderService orderService;
    private final SeckillMQProducer dlqProducer;
    private final ObjectMapper objectMapper;

    /**
     * 消费秒杀订单消息
     * concurrency = "16" 启动 16 个消费线程(按 partition 选择)
     */
    @KafkaListener(
        topics = "seckill_orders",
        groupId = "seckill_order_consumer_group",
        concurrency = "16",
        containerFactory = "seckillKafkaListenerContainerFactory"
    )
    public void onSeckillOrderMessage(ConsumerRecord<String, String> record, 
                                       Acknowledgment ack) {
        SeckillOrderMessage orderMsg;
        try {
            // 反序列化
            orderMsg = objectMapper.readValue(record.value(), SeckillOrderMessage.class);
        } catch (Exception e) {
            log.error("消息解析失败: partition={}, offset={}, value={}", 
                      record.partition(), record.offset(), record.value(), e);
            ack.acknowledge(); // 无法解析的消息直接确认跳过
            return;
        }

        // 消费逻辑 + 重试
        try {
            boolean success = processOrderWithRetry(orderMsg, 0);
            if (success) {
                ack.acknowledge(); // 消费成功,确认 offset
            } else {
                // 重试失败,进入死信队列
                log.error("订单消费重试次数耗尽: orderToken={}", orderMsg.getToken());
                dlqProducer.sendToDLQ(orderMsg);
                ack.acknowledge(); // 确认跳过,避免阻塞
            }
        } catch (Exception e) {
            log.error("订单消费异常: orderToken={}", orderMsg.getToken(), e);
            // 不确认,等待重试(Kafka rebalance 会重新投递)
        }
    }

    /**
     * 带重试的订单处理
     */
    private boolean processOrderWithRetry(SeckillOrderMessage msg, int retryCount) {
        try {
            orderService.createOrder(msg);
            return true;
        } catch (DuplicateKeyException e) {
            // 幂等:该订单已处理过(可能是消费者重启导致重复投递)
            log.warn("订单已存在,跳过: {}", msg.getOrderToken());
            return true; // 视为成功
        } catch (Exception e) {
            if (retryCount < 3) {
                // 重试:指数退避等待
                try {
                    Thread.sleep((long) Math.pow(2, retryCount) * 1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
                return processOrderWithRetry(msg, retryCount + 1);
            }
            return false;
        }
    }
}
// KafkaConsumerConfig.java - 消费者配置
package com.seckill.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ContainerProperties;

import java.util.HashMap;
import java.util.Map;

@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> 
            seckillKafkaListenerContainerFactory() {

        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9092,kafka-2:9092,kafka-3:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "seckill_order_consumer_group");
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交 offset
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);       // 每次最多拉 500 条
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30000); // 消费超时时间
        props.put(ConsumerConfig.DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        DefaultKafkaConsumerFactory<String, String> consumerFactory = 
            new DefaultKafkaConsumerFactory<>(props);

        ConcurrentKafkaListenerContainerFactory<String, String> factory = 
            new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory);
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        
        return factory;
    }
}

6.5 有序消费保证

┌──────────────────────────────────────────────────────────┐
│                   消息有序性设计                           │
│                                                          │
│  问题场景:                                               │
│  用户 A 先后购买了商品 X 和商品 Y                          │
│  如果两个消息被不同消费者/分区处理                           │
│  可能 Y 的订单先于 X 创建                                 │
│                                                          │
│  解决方案:                                               │
│  1. 使用 activity_id + user_id 作为 partition key         │
│     保证同一用户的消息进入同一分区                           │
│  2. 同一分区内只有一个消费者顺序消费                         │
│  3. 消费者处理成功后严格递增提交 offset                     │
│  4. Consumer 数量 ≤ Partition 数量(避免 idle 消费组)      │
│                                                          │
│  分区数规划:                                              │
│  - 64 个 partition                                        │
│  - 最大 64 个 consumer 并行消费                             │
│  - 吞吐量:64 partitions × 10W msg/s = 640W msg/s         │
│  - 远超秒杀实际需要的吞吐量                                 │
│                                                          │
└──────────────────────────────────────────────────────────┘

6.6 延迟消息处理

// SeckillOrderTimeoutService.java - 订单超时取消服务
package com.seckill.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.Set;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillOrderTimeoutService {

    private final StringRedisTemplate redisTemplate;
    private final SeckillMQProducer mqProducer;

    /**
     * 订单超时取消(15 分钟未支付则取消并回滚库存)
     * 方案一:使用 RocketMQ 的延时消息级别 level=16(对应 15min)
     * 方案二:使用 Redis 有序集合实现简易延时队列
     */

    /**
     * 设置订单超时定时(活动开始时调用)
     */
    public void scheduleOrderTimeout(String orderToken) {
        // 添加到延时队列(分值 = 当前时间戳 + 15 分钟)
        double score = System.currentTimeMillis() + 15 * 60 * 1000L;
        redisTemplate.opsForZSet().add("seckill:order_timeout", orderToken, score);
    }

    /**
     * 定时扫描超时的订单(每 10 秒执行一次)
     */
    @Scheduled(fixedRate = 10000)
    public void scanTimeoutOrders() {
        long now = System.currentTimeMillis();

        // 扫描分值 ≤ 当前时间的订单(即已超时)
        Set<String> timeoutOrders = redisTemplate.opsForZSet()
            .rangeByScore("seckill:order_timeout", 0, now, 0, 100);

        if (timeoutOrders == null || timeoutOrders.isEmpty()) {
            return;
        }

        log.info("扫描到超时订单数: {}", timeoutOrders.size());

        for (String orderToken : timeoutOrders) {
            // 检查订单状态
            String status = redisTemplate.opsForValue()
                .get("seckill:order_status:" + orderToken);
            if ("pending".equals(status)) {
                // 订单仍未支付 → 触发库存回滚
                log.info("订单超时未支付,触发回滚: {}", orderToken);
                // 发送库存回滚 MQ 消息
                mqProducer.sendStockCompensate(orderToken);
                // 从延时队列移除(原子操作防止重复处理)
                redisTemplate.opsForZSet().remove("seckill:order_timeout", orderToken);
            } else {
                // 订单已支付/取消,直接从队列清除
                redisTemplate.opsForZSet().remove("seckill:order_timeout", orderToken);
            }
        }
    }
}

七、数据库层设计

数据库层是秒杀系统的最后一道防线,也是整个架构中最不能出错的一环。为什么?因为数据库里存储的是最终事实——订单信息、库存余额、资金流水。这些数据如果出错,不仅影响用户体验,还可能引发法律纠纷和监管问题。

说到数据库层,有一个常见的认知偏差:很多团队认为「既然有了 Redis 预扣,数据库随便写就行了」。这是一个危险的假设。事实上,数据库在秒杀场景中扮演着三个不可替代的角色:

  1. 最终一致性保障:Redis 是内存结构,存在主从切换、宕机重启导致数据丢失的风险。数据库有 binlog 和 redo log 保证数据持久性,是唯一可以当作「唯一真相源」的地方。

  2. 业务完整性约束:数据库的 unique key、foreign key、check 约束是防止脏数据的最后一道屏障。即使上层逻辑有 bug,数据库层面也能拦截绝大多数异常。

  3. 事后审计:秒杀结束后,财务、法务、运营可能需要查询完整的订单链路。数据库是对账和审计的唯一可靠来源。

因此我们在设计上不惜代价保证数据库层的正确性:使用半同步复制防止主从切换丢数据,使用 ROW 格式 binlog 方便数据回溯,使用乐观锁 + 限量 WHERE 条件确保任何情况下 sold_quantity 都不会超过 total_quantity。

分库分表是 MySQL 在秒杀场景下的标配。为什么?因为单表的并发写入能力有限(通常 3000~5000 TPS),而秒杀需要承受 1W+ TPS 的订单写入。我们将订单表按 user_id 分为 16 个库 × 32 张表 = 512 张分表,每张表正常状态下行数不超过百万级,索引和数据页都能充分缓存,性能可控。

7.1 MySQL 集群架构

┌──────────────────────────────────────────────────────────────┐
│                    MySQL 高可用集群                            │
│                                                              │
│              ┌──────────────┐                                │
│              │  ProxySQL    │  (读写分离代理)                  │
│              │  (中间件)     │                                │
│              └──────┬───────┘                                │
│                     │                                        │
│         ┌───────────┼───────────┐                            │
│         │      写请求  │  读请求   │                           │
│         ▼           │           ▼                            │
│  ┌──────────────┐   │   ┌──────────────┐                    │
│  │  Master (写) │   │   │  Slave-1 (读) │                    │
│  │  (半同步复制) │   │   │              │                    │
│  └──────┬───────┘   │   └──────────────┘                    │
│         │           │   ┌──────────────┐                    │
│         └───────────┼──▶│  Slave-2 (读) │                    │
│              同步复制  │   │              │                    │
│                     │   └──────────────┘                    │
│                     │   ┌──────────────┐                    │
│                     │   │  Slave-3 (读) │                    │
│                     │   │  (专用统计)    │                    │
│                     │   └──────────────┘                    │
│                                                              │
│  配置:                                                       │
│  - InnoDB 引擎                                              │
│  - 半同步复制(semi-sync)                                    │
│  - binlog_format = ROW(ROW 格式,安全)                      │
│  - transaction_isolation = READ-COMMITTED                     │
│  - innodb_lock_wait_timeout = 5                               │
│  - innodb_flush_log_at_trx_commit = 1 (写操作)               │
│  - innodb_buffer_pool_size = 物理内存 70-80%                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

7.2 分库分表设计

7.2.1 订单表分片

– 用户订单表(按 user_id 分片,保证用户查询自己订单时只扫一片)

-- 原始表结构
-- db: seckill_order_{0..15}  (16 个库)
-- 每库: t_order_{0..31}      (每库 32 张表,共 512 张表)

-- 每张表结构
CREATE TABLE `t_order_0` (
    `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `order_token`     VARCHAR(64)     NOT NULL COMMENT '订单唯一标识',
    `activity_id`     VARCHAR(32)     NOT NULL COMMENT '活动 ID',
    `user_id`         BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
    `item_id`         BIGINT UNSIGNED NOT NULL COMMENT '商品 ID',
    `quantity`        INT UNSIGNED    NOT NULL DEFAULT 1 COMMENT '数量',
    `price`           DECIMAL(10,2)   NOT NULL COMMENT '单价',
    `total_amount`    DECIMAL(10,2)   NOT NULL COMMENT '总金额',
    `status`          TINYINT         NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消 3-已完成',
    `version`         INT UNSIGNED    NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
    `create_time`     DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `update_time`     DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    `pay_time`        DATETIME(3)     DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_token` (`order_token`),
    KEY `idx_user_id_create` (`user_id`, `create_time`),
    KEY `idx_activity_id` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 
  ROW_FORMAT=COMPRESSED
  COMMENT='秒杀订单表';

7.2.2 库存表设计

-- 库存表(独立库,不与订单混在一起)
-- db: seckill_stock  (独立集群)

CREATE TABLE `t_stock` (
    `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `activity_id`     VARCHAR(32)     NOT NULL COMMENT '活动 ID',
    `item_id`         BIGINT UNSIGNED NOT NULL COMMENT '商品 ID',
    `total_quantity`  INT UNSIGNED    NOT NULL COMMENT '总库存',
    `sold_quantity`   INT UNSIGNED    NOT NULL DEFAULT 0 COMMENT '已售数量',
    `locked_quantity` INT UNSIGNED    NOT NULL DEFAULT 0 COMMENT '锁定库存',
    `version`         INT UNSIGNED    NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
    `status`          TINYINT         NOT NULL DEFAULT 1 COMMENT '1-正常 2-售罄 3-终止',
    `create_time`     DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `update_time`     DATETIME(3)     NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_activity_item` (`activity_id`, `item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 
  COMMENT='库存表';

-- 库存流水表(用于对账和最终一致性校验)
CREATE TABLE `t_stock_log` (
    `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `activity_id`     VARCHAR(32)     NOT NULL,
    `user_id`         BIGINT UNSIGNED NOT NULL,
    `item_id`         BIGINT UNSIGNED NOT NULL,
    `order_token`     VARCHAR(64)     NOT NULL,
    `operation`       VARCHAR(16)     NOT NULL COMMENT 'DEDUCT/ROLLBACK',
    `quantity`        INT             NOT NULL,
    `before_stock`    INT UNSIGNED    NOT NULL,
    `after_stock`     INT UNSIGNED    NOT NULL,
    `create_time`     DATETIME(3)     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_op` (`order_token`, `operation`),
    KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='库存流水日志';

7.3 DB 库存扣减(兜底方案)

当 Redis 预扣成功后,最终订单消
费流程中需要在 DB 层完成最终的库存校验与扣减。这是防止超卖的最后一道防线

-- 方式一:乐观锁(推荐,冲突少)
UPDATE t_stock 
SET sold_quantity = sold_quantity + 1, 
    version = version + 1
WHERE activity_id = ? 
  AND item_id = ? 
  AND sold_quantity + locked_quantity < total_quantity
  AND version = ?;

-- 影响行数 = 1 表示成功,= 0 表示失败(库存不足或并发冲突)

-- 方式二:悲观锁(冲突多时使用)
BEGIN;
SELECT sold_quantity, locked_quantity, total_quantity, version
FROM t_stock 
WHERE activity_id = ? AND item_id = ?
FOR UPDATE;

-- 应用层判断 sold_quantity + locked_quantity < total_quantity
UPDATE t_stock 
SET sold_quantity = sold_quantity + 1
WHERE activity_id = ? AND item_id = ?;
COMMIT;

推荐用乐观锁(方式一),原因:

  • 秒杀用户量大但成功率低,冲突概率不高
  • 配合 Redis 层已将大部分流量过滤,DB 并发有限
  • 悲观锁的 FOR UPDATE 在热点行容易造成锁等待

7.4 消费者创建订单流程

消费者创建订单流程:

// SeckillOrderService.java - 订单创建服务(消费者侧)
package com.seckill.service;

import com.seckill.dto.SeckillOrderMessage;
import com.seckill.entity.SeckillOrder;
import com.seckill.entity.Stock;
import com.seckill.entity.StockLog;
import com.seckill.mapper.SeckillOrderMapper;
import com.seckill.mapper.StockLogMapper;
import com.seckill.mapper.StockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillOrderService {

    private final SeckillOrderMapper orderMapper;
    private final StockMapper stockMapper;
    private final StockLogMapper stockLogMapper;
    private final StringRedisTemplate redisTemplate;

    /**
     * 创建订单:DB 层最终库存校验 + 订单落库
     */
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(SeckillOrderMessage msg) {

        // 1. DB 层库存校验与扣减(乐观锁机制)
        int affected = stockMapper.deductStock(msg.getActivityId(), msg.getItemId());

        if (affected == 0) {
            log.warn("DB库存不足: activityId={}, userId={}", msg.getActivityId(), msg.getUserId());
            rollbackRedisStock(msg);
            throw new BusinessException("库存已售罄");
        }

        // 2. 查询当前库存(用于流水记录)
        Stock currentStock = stockMapper.selectByActivityAndItem(
            msg.getActivityId(), msg.getItemId());
        int beforeStock = currentStock.getTotalQuantity() - currentStock.getSoldQuantity() + 1;
        int afterStock = beforeStock - 1;

        // 3. 插入订单
        try {
            SeckillOrder order = new SeckillOrder();
            order.setOrderToken(msg.getOrderToken());
            order.setActivityId(msg.getActivityId());
            order.setUserId(msg.getUserId());
            order.setItemId(msg.getItemId());
            order.setQuantity(1);
            order.setPrice(new BigDecimal("99.00"));
            order.setTotalAmount(new BigDecimal("99.00"));
            order.setStatus(0);
            order.setCreateTime(LocalDateTime.now());
            order.setUpdateTime(LocalDateTime.now());
            orderMapper.insert(order);
        } catch (DuplicateKeyException e) {
            log.warn("订单已存在,跳过: {}", msg.getOrderToken());
            return;
        }

        // 4. 记录库存流水
        StockLog stockLog = new StockLog();
        stockLog.setActivityId(msg.getActivityId());
        stockLog.setUserId(msg.getUserId());
        stockLog.setItemId(msg.getItemId());
        stockLog.setOrderToken(msg.getOrderToken());
        stockLog.setOperation("DEDUCT");
        stockLog.setQuantity(1);
        stockLog.setBeforeStock(beforeStock);
        stockLog.setAfterStock(afterStock);
        stockLog.setCreateTime(LocalDateTime.now());
        stockLogMapper.insert(stockLog);

        // 5. 更新 Redis 订单状态
        redisTemplate.opsForValue().set(
            "seckill:order_status:" + msg.getOrderToken(),
            "success",
            Duration.ofHours(24)
        );

        log.info("订单创建成功: orderToken={}, userId={}, activityId={}",
                 msg.getOrderToken(), msg.getUserId(), msg.getActivityId());
    }

    private void rollbackRedisStock(SeckillOrderMessage msg) {
        String stockKey = "seckill:stock:" + msg.getActivityId();
        redisTemplate.opsForValue().increment(stockKey);
        String buyerKey = "seckill:buyers:" + msg.getActivityId();
        redisTemplate.opsForSet().remove(buyerKey, String.valueOf(msg.getUserId()));
        log.info("Redis库存已回滚: activityId={}, userId={}", msg.getActivityId(), msg.getUserId());
    }
}
// StockMapper.java - MyBatis Mapper
package com.seckill.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.seckill.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface StockMapper extends BaseMapper<Stock> {

    /**
     * DB 层乐观锁库存扣减(兜底防线)
     * WHERE 含 version 校验,与 7.3 节方式一一致:影响行数=1 成功,=0 库存不足或并发冲突
     */
    @Update("UPDATE t_stock SET sold_quantity = sold_quantity + 1, " +
            "version = version + 1, update_time = NOW(3) " +
            "WHERE activity_id = #{activityId} AND item_id = #{itemId} " +
            "AND (sold_quantity + locked_quantity) < total_quantity " +
            "AND version = #{version} " +
            "AND status = 1")
    int deductStock(@Param("activityId") String activityId,
                    @Param("itemId") Long itemId,
                    @Param("version") Integer version);
}
// SeckillOrder.java - 订单实体(MyBatis-Plus)
package com.seckill.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("t_order")
public class SeckillOrder {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String orderToken;
    private String activityId;
    private Long userId;
    private Long itemId;
    private Integer quantity;
    private BigDecimal price;
    private BigDecimal totalAmount;
    private Integer status;
    private LocalDateTime payTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    @Version
    private Integer version;
}

7.5 异步对账与最终一致性

┌──────────────────────────────────────────────────────────┐
│                一致性保障架构                              │
│                                                          │
│  实时层:                                                 │
│  ┌─────────┐     ┌──────────┐     ┌──────────────┐     │
│  │ Redis   │ ──▶ │ MQ       │ ──▶ │ MySQL        │     │
│  │ 预扣库存 │     │ 可靠投递  │     │ 最终落库     │     │
│  └─────────┘     └──────────┘     └────────────┘     │
│       │                                      │          │
│       │         可能的不一致场景               │          │
│       │         - MQ 消费失败                 │          │
│       │         - DB 写入失败                 │          │
│       │         - 网络超时                    │          │
│       │                                      │          │
│  补偿层:                                                 │
│  ┌──────────────────────────────────────────┐            │
│  │         异步对账 Job(每 5 分钟)           │            │
│  │                                          │            │
│  │  1. 扫描 Redis(已抢购用户)               │            │
│  │  2. 对比 MySQL(已创建订单)               │            │
│  │  3. 对不齐的 → 触发补偿                    │            │
│  │     - Redis 有 → DB 没有 → 补写入 DB       │            │
│  │     - Redis 有 → 写入失败 → 回滚 Redis     │            │
│  └──────────────────────────────────────────┘            │
│                                                          │
│  对账逻辑:                                               │
│  SET A = Redis:buyers:{activity}(抢购成功的用户集合)      │
│  SET B = SELECT user_id FROM t_order WHERE activity = x  │
│  差集 A-B 需要补单 | 差集 B-A 需要释放 Redis 库存          │
│                                                          │
└──────────────────────────────────────────────────────────┘

八、全链路流程详解

8.1 秒杀完整时序

Client         CDN/Gateway      Pre-filter      Seckill-Svc      Redis        MQ          Order-Svc       DB
  │                │                │                │              │            │              │            │
  │ ①预热请求      │                │                │              │            │              │            │
  │──────────────▶│                │                │              │            │              │            │
  │               │                │                │ ②加载活动+库存│            │              │            │
  │               │                │                │─────────────▶│            │              │            │
  │               │                │                │ ③加载完成     │            │              │            │
  │               │                │                │◀─────────────│            │              │            │
  │               │                │                │              │            │              │            │
  │ ④活动开始      │                │                │              │            │              │            │
  │ (倒计时结束)   │                │                │              │            │              │            │
  │──────────────▶│                │                │              │            │              │            │
  │               │ ⑤静态化页面    │                │              │            │              │            │
  │               │   服务正常返回  │                │              │            │              │            │
  │               │◀───────────────│                │              │            │              │            │
  │               │                │                │              │            │              │            │
  │ ⑥用户点击抢购  │                │                │              │            │              │            │
  │──────────────▶│ ⑦限流放行      │                │              │            │              │            │
  │               │───────────────▶│ ⑧Token校验     │              │            │              │            │
  │               │                │───────────────▶│ ⑨Lua扣减      │            │              │            │
  │               │                │                │─────────────▶│            │              │            │
  │               │                │                │ ⑩扣减成功     │            │              │            │
  │               │                │                │◀─────────────│            │              │            │
  │               │                │                │              │            │              │            │
  │               │                │                │ ⑪发MQ(异步)   │            │              │            │
  │               │                │                │──────────────────────────▶│              │            │
  │               │                │                │              │            │              │            │
  │ ⑫返回"抢购成功"│                │                │              │            │              │            │
  │◀──────────────│◀───────────────│◀───────────────│              │            │              │            │
  │               │                │                │              │            │              │            │
  │ ⑬轮询订单状态  │                │                │              │            │ ⑭消费MQ消息   │            │
  │──────────────────────────────────────────────────▶│              │            │─────────────▶│            │
  │               │                │                │              │            │              │ ⑮开启事务    │
  │               │                │                │              │            │              │──────────▶  │ 
  │               │                │                │              │            │              │ ⑯DB扣库存   │
  │               │                │                │              │            │              │──────────▶  │
  │               │                │                │              │            │              │ ⑰创建订单   │
  │               │                │                │              │            │              │──────────▶  │
  │               │                │                │              │            │              │ ⑱提交事务   │
  │               │                │                │              │            │              │──────────▶  │
  │               │                │                │              │            │              │            │
  │               │                │                │ ⑲状态变为成功 │            │              │            │
  │◀──────────────────────────────────────────────────────────────│            │              │            │
  │               │                │                │              │            │              │            │

8.2 用户端交互时序

Client                                                        Server
  │                                                              │
  │  1. 访问活动页面                                              │
  │  GET /seckill/activity/1001                                   │
  │─────────────────────────────────────────────────────────────▶│
  │                                                              │
  │  2. 返回静态 HTML + 活动状态(未开始,显示倒计时)             │
  │◀─────────────────────────────────────────────────────────────│
  │                                                              │
  │  3. 倒计时期间:JS 执行倒计时 + SSE 监听 "start" 事件         │
  │                                                      (WAIT)  │
  │                                                              │
  │  4. 倒计时结束:SSE 推送 "START"                               │
  │◀───────────────────────────────── (WebSocket/SSE)            │
  │                                                              │
  │  5. 激活抢购按钮                                               │
  │  [用户点击"立即抢购"]                                          │
  │                                                              │
  │  6. 先获取购买资格(Token)                                    │
  │  POST /seckill/token { activity_id: 1001, user_id, sign }     │
  │─────────────────────────────────────────────────────────────▶│
  │  7. 返回 Token(如获取成功则扣除一个令牌池名额)                │
  │◀─────────────────────────────────────────────────────────────│
  │                                                              │
  │  8. 执行秒杀                                                  │
  │  POST /seckill/execute { activity_id, token, item_id, sign }  │
  │─────────────────────────────────────────────────────────────▶│
  │  9. 返回 order_token + "抢购成功,正在处理订单"               │
  │◀─────────────────────────────────────────────────────────────│
  │                                                              │
  │  10. 轮询订单状态(每 500ms,最多 2s)                         │
  │  GET /seckill/result?order_token=xxx                          │
  │─────────────────────────────────────────────────────────────▶│
  │  11. 返回订单状态:pending / success / fail                   │
  │◀─────────────────────────────────────────────────────────────│
  │                                                              │
  │  12. 停止轮询 → 展示订单详情/支付页面                          │
  │                                                              │

九、关键细节设计

前面几章我们从宏观到微观逐层了架构的各个组件,这一章回归细节——那些看似细微却足以左右系统成败的设计点。

在实际的秒杀系统工程中,架构图上的每一个箭头和方框都对应着数量繁多的技术决策。比如:Token 池的放大系数设为 5 倍还是 10 倍?库存分桶选 10 个还是 20 个?MQ 消费重试 3 次还是 5 次?这些参数的选择没有绝对的对错,只有与业务适配的合理性。但更重要的是理解这些参数背后的推导逻辑——当你面对一个未曾见过的新场景时,能够基于核心原则推导出最优解,而不是依赖经验值。

本章选取了六个最关键的细节:流量漏斗逐层过滤的效率计算、Token 机制放大系数的经济学推导、超卖防护三层保障的必要性证明、幂等性在分布式系统中的实现模式、库存回滚场景的决策树、以及反作弊体系的多层次构建思路。每一个细节都值得展开讨论。

9.1 流量漏斗逐层过滤

QPS   层级                      拦截手段                        通过率    剩余 QPS
──────────────────────────────────────────────────────────────────────────
1000W  入口原始流量                   -                           100%      1000W
  │
  ▼
 500W   CDN/边缘                    静态化页面+资源缓存过滤             50%       500W
  │                                 真正的"点击"才会穿透
  ▼
 200W   DDoS 高防/WAF            恶意 IP 拦截 + CC 防护                40%       200W
  │
  ▼
 100W   API 网关限流                 全局限流 100W + 用户/IP 维度限流       50%       100W
  │
  ▼
  50W   前置过滤                     Token 校验 + 参数校验 + 用户黑名单     25%        50W
  │
  ▼
  10W   Redis Lua 扣减              本地库存标记快速过滤 + Redis 原子扣减    ~1%        10W
  │
  ▼
1.5W   MQ 下单                     Kafka 异步消费 + 顺序处理              -         1.5W
  │
  ▼
1.5W   数据库落库                   最终订单创建                          100%      1.5W
  │
  ▼
1.5W   成功订单(假设库存5000件)                                                        5000

9.2 Token 机制详解

┌──────────────────────────────────────────────────────────────┐
│                     Token 获取与验证流程                       │
│                                                              │
│  令牌池容量 = 商品库存 × 放大系数 (5~10 倍)                    │
│  库存 5000 件 → 令牌池 30000 个                                │
│                                                              │
│  为什么设放大系数?                                             │
│  - 不是获取令牌的用户都会点击秒杀(流失)                         │
│  - 放大系数 5~10 使得:                                        │
│    令牌池耗尽 → 只有约 5-10% 的流量走到 Redis 扣减              │
│                                                              │
│  令牌生命周期:                                                │
│  1. 用户进入秒杀页 → 请求获取令牌(异步,不影响体验)             │
│  2. 获取成功:用户持有 token,有效期 10 分钟                     │
│  3. 用户执行秒杀:携带 token 发送请求                            │
│  4. 秒杀完成后:token 标记为"已使用"(atomic SET 防止重复用)      │
│  5. 如果 10 分钟未使用:token 自动过期,释放令牌池                 │
│                                                              │
│  令牌状态机:                                                  │
│  UNAVAILABLE (池耗尽)                                           │
│       │                                                       │
│       ▼                                                       │
│  AVAILABLE ──[领取]──▶ HELD ──[使用]──▶ CONSUMED            │
│       ▲                        │                              │
│       │                        └─[超时 10min]──▶ EXPIRED    │
│       │                                    │                  │
│       └────────────[释放回池]────────────────┘                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

9.3 超卖防护三层保障

┌──────────────────────────────────────────────────────────────┐
│                    超卖防护三道防线                            │
│                                                              │
│  第一道防线(Redis 层 —— 拦截 99%):                          │
│  ┌────────────────────────────────────────────────────┐     │
│  │ Redis Lua 脚本原子扣减                               │     │
│  │ - GET + DECR 在一个 Redis 原子命令中完成              │     │
│  │ - 使用 decr 后判断 < 0 即回滚                         │     │
│  │ - 这里不会超卖(Redis 单线程模型)                    │     │
│  └────────────────────────────────────────────────────┘     │
│                          │                                    │
│                          ▼                                    │
│  第二道防线(MQ 层 —— 顺序消费):                              │
│  ┌────────────────────────────────────────────────────┐     │
│  │ 按 user_id / activity_id 分区                        │     │
│  │ 同一活动 ID 内的消息严格顺序消费                       │     │
│  │ 保证库存扣减的同序性                                   │     │
│  └────────────────────────────────────────────────────┘     │
│                          │                                    │
│                          ▼                                    │
│  第三道防线(DB 层 —— 兜底保障):                              │
│  ┌────────────────────────────────────────────────────┐     │
│  │ UPDATE ... WHERE sold + locked < total              │     │
│  │ 数据库 UPDATE 的 WHERE 条件包含库存上限约束            │     │
│  │ 任何并发更新都不会让 sold 超过 total                   │     │
│  │ 受影响行数 = 0 即表示库存冲突,直接返回失败              │     │
│  └────────────────────────────────────────────────────┘     │
│                                                              │
│  三重保障下:即使 Redis 出现极端 bug(如主从切换数据丢失),      │
│  DB 层仍然能确保 sold_quantity ≤ total_quantity                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

9.4 幂等性设计

┌──────────────────────────────────────────────────────────────┐
│                     幂等性保障措施                             │
│                                                              │
│  维度 1:用户重复提交幂等                                       │
│  ────────────────────────                                      │
│  - Gateway 层分配 request_id,Redis SET NX 5秒防重             │
│  - 秒杀接口维度的唯一键: (user_id, activity_id)               │
│  - Redis SETNX "seckill:dedup:{user_id}:{activity_id}" 1 5  │
│  - 命中 → 直接返回之前的结果(不会重复扣减)                    │
│                                                              │
│  维度 2:MQ 消费幂等                                            │
│  ─────────────────────                                         │
│  - 消息携带全局唯一的 order_token                              │
│  - DB 唯一键 order_token 防重插入                              │
│  - 消费逻辑检查:                                           │
│    if EXISTS order_token → SKIP (已处理过)                   │
│    else → PROCESS + INSERT                                  │
│                                                              │
│  维度 3:库存回滚幂等                                           │
│  ────────────────────                                          │
│  - 库存流水表唯一键 (order_token, operation)                  │
│  - 回滚前先检查是否已回滚:SELECT 1 FROM stock_log             │
│    WHERE order_token = ? AND operation = 'ROLLBACK'          │
│  - 不存在 → 执行回滚并插入流水记录                             │
│  - 已存在 → 跳过(已回滚过)                                    │
│                                                              │
│  维度 4:接口签名防篡改 + 时间戳防重放                            │
│  ────────────────────────────────                               │
│  - 客户端在请求头携带 X-Timestamp + X-Signature              │
│  - X-Signature = HMAC-SHA256(params + SECRET_KEY, body)     │
│  - 服务端校验:                                               │
│    1. X-Timestamp 在 ±5 分钟内 → 否则拒绝                    │
│    2. X-Signature 正确 → 否则拒绝                             │
│    3. 同时维护一个时间窗口内已使用的 nonce SET 防重            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

9.5 库存回滚机制

┌──────────────────────────────────────────────────────────────┐
│                    库存回滚场景与处理                           │
│                                                              │
│  场景 1:订单超时未支付(最常见)                                │
│  ────────────────────────────                                 │
│  - 用户 15 分钟内未支付                                        │
│  - 定时任务扫描 seckill:order_timeout ZSET                    │
│  - 发送到 seckill_stock_compensate topic                      │
│  - 消费者执行:                                                 │
│    1. 更新订单状态为 CANCELLED                                 │
│    2. DB 库存回滚:sold_quantity = sold_quantity - 1          │
│    3. Redis 库存回滚:INCR seckill:stock:1001                │
│    4. 记录流水日志                                            │
│                                                              │
│  场景 2:MQ 消费失败(无法下单)                                 │
│  ────────────────────────────                                 │
│  - 消费失败 3 次后进入 DLQ                                    │
│  - 兜底处理:                                                   │
│    1. 检查是否是库存本身不足(查询 DB 确认)                     │
│    2. 确认为系统异常 → 触发人工介入 / 自动重试                   │
│    3. 成功后回滚:释放 Redis 库存 + 标记失败                     │
│                                                              │
│  场景 3:Redis 与 DB 不一致(定时对账发现)                       │
│  ──────────────────────────────────                           │
│  - 对账发现:Redis 已扣减但 DB 没有对应订单                     │
│  - 可能原因:MQ 消息丢失 / 消费异常                             │
│  - 处理:根据业务选择                                          │
│    A. 补单(保持用户体验优先):INSERT t_order + 写流水          │
│    B. 回滚(库存准确性优先):INCR Redis 库存                    │
│  - 一般选 A(补单)+ 记录告警以便排查根因                       │
│                                                              │
│  注意事项:                                                    │
│  1. 库存回滚后用户又能抢购?                                    │
│     → 不重新开放购买机会给用户,只回滚 Redis 库存给后续用户       │
│  2. 回滚和抢购的并发竞争?                                      │
│     → 所有操作通过 Redis Lua,天然串行化                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

9.6 用户黑名单与防刷

// AntiCheatService.java - 反作弊服务
package com.seckill.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class AntiCheatService {

    private final StringRedisTemplate redisTemplate;

    /**
     * 同 IP 段计数限流
     */
    public boolean checkIpPrefixLimit(String clientIp) {
        String ipPrefix = getIPPrefix(clientIp);
        String key = "seckill:ip_prefix:" + ipPrefix;
        
        Long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(10));
        }
        return count <= 100; // 同一 /24 IP 段最多 100 次/10s
    }

    /**
     * 用户行为模式检测
     */
    public boolean checkBehaviorPattern(Long userId, List<UserAction> actionLog) {
        // 1. 检测连续请求时间差过短(< 50ms = 机器人特征)
        if (actionLog.size() >= 2) {
            long interval = actionLog.get(actionLog.size() - 1).getTimestamp()
                          - actionLog.get(actionLog.size() - 2).getTimestamp();
            if (interval < 50) {
                log.warn("疑似机器人: userId={}, interval={}ms", userId, interval);
                return false;
            }
        }

        // 2. 检测短时间内密集操作(一分钟内浏览/操作 > 20 次)
        String recentKey = "seckill:user_actions:" + userId;
        Long recentCount = redisTemplate.opsForList().size(recentKey);
        if (recentCount != null && recentCount > 20) {
            log.warn("用户操作频次异常: userId={}, count={}", userId, recentCount);
            return false;
        }

        return true;
    }

    /**
     * 账号风险等级校验
     */
    public boolean isAccountRisky(Long userId) {
        // 新注册账号(< 24h)施加更严格限流
        String regTimeKey = "user:reg_time:" + userId;
        String regTime = redisTemplate.opsForValue().get(regTimeKey);
        if (regTime != null && Long.parseLong(regTime) > System.currentTimeMillis() - 86400000) {
            // 新账号:更频繁检查
            String newUserKey = "seckill:new_user_limit:" + userId;
            Long count = redisTemplate.opsForValue().increment(newUserKey);
            if (count == 1) {
                redisTemplate.expire(newUserKey, Duration.ofSeconds(1));
            }
            return count > 2; // 新账号限流 2 次/秒
        }
        return false;
    }

    /**
     * 黑名单查询
     */
    public boolean isBlacklisted(String ip, Long userId) {
        return Boolean.TRUE.equals(redisTemplate.opsForSet()
                .isMember("seckill:blacklist:ip", ip))
            || Boolean.TRUE.equals(redisTemplate.opsForSet()
                .isMember("seckill:blacklist:user", String.valueOf(userId)));
    }

    private String getIPPrefix(String ip) {
        String[] parts = ip.split("\\.");
        return parts[0] + "." + parts[1] + "." + parts[2];
    }

    /**
     * 黑名单数据结构 (Redis Set)
     * 
     * seckill:blacklist:ip   → set of banned IPs
     * seckill:blacklist:user → set of banned user IDs
     */
}

## 十、监控与告警

### 10.1 监控指标体系

┌──────────────────────────────────────────────────────────────┐
│ 监控指标金字塔 │
│ │
│ 业务指标(黄金指标): │
│ ┌────────────────────────────────────────────────────┐ │
│ │ seckill_qps_total : 秒杀总 QPS │ │
│ │ seckill_qps_pass : 通过限流/扣减的 QPS │ │
│ │ seckill_success_count : 秒杀成功累计数 │ │
│ │ seckill_fail_count : 秒杀失败累计数 │ │
│ │ seckill_stock_remaining : 剩余库存 (Redis) │ │
│ │ order_created_count : DB 订单创建数 │ │
│ │ order_success_rate : 订单创建成功率 │ │
│ │ consistency_gap | Redis - DB 差值(对账用) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 基础设施指标: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ gateway_latency_p99 : 网关层 P99 延迟 │ │
│ │ redis_ops_latency : Redis 操作延迟 │ │
│ │ redis_memory_used | Redis 内存使用率 │ │
│ │ redis_cpu_usage | Redis CPU 使用率 │ │
│ │ kafka_lag | Kafka 消费延迟 │ │
│ │ kafka_producer_rate | MQ 生产速率 │ │
│ │ kafka_consumer_rate | MQ 消费速率 │ │
│ │ mysql_qps | MySQL 查询 QPS │ │
│ │ mysql_tps | MySQL 事务 TPS │ │
│ │ mysql_slow_queries | 慢查询数量 │ │
│ │ db_connection_pool | 连接池使用率 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 异常指标: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ rate_limit_rejected | 限流拒绝数 │ │
│ │ circuit_breaker_open | 熔断器打开次数 │ │
│ │ mq_consume_fail | MQ 消费失败 │ │
│ │ mq_dlq_count | 死信队列堆积 │ │
│ │ cache_miss_rate | 缓存命中率 │ │
│ │ insert_conflict_count | DB 唯一键冲突数 │ │
│ │ panic_count | 服务 panic 数 │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘


### 10.2 Prometheus + Grafana 监控配置

```java
// SeckillMetrics.java - 秒杀指标采集(Micrometer + Prometheus)
package com.seckill.metrics;

import io.micrometer.core.instrument.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class SeckillMetrics {

    private final MeterRegistry meterRegistry;

    /** 秒杀请求总 QPS(按活动和状态分类) */
    public void recordSeckillRequest(String activityId, String status) {
        Counter.builder("seckill.qps.total")
                .description("秒杀请求总 QPS")
                .tag("activity_id", activityId)
                .tag("status", status)
                .register(meterRegistry)
                .increment();
    }

    /** 秒杀请求延迟(按步骤分类) */
    public void recordSeckillLatency(String step, long latencyMs) {
        Timer.builder("seckill.latency.ms")
                .description("秒杀请求延迟")
                .tag("step", step)
                .publishPercentiles(0.5, 0.95, 0.99)
                .register(meterRegistry)
                .record(latencyMs, TimeUnit.MILLISECONDS);
    }

    /** 设置剩余库存(Gauge) */
    public void setStockRemaining(String activityId, long stock) {
        Gauge.builder("seckill.stock.remaining", () -> stock)
                .description("剩余库存")
                .tag("activity_id", activityId)
                .register(meterRegistry);
    }

    /** Kafka 消费积压(Gauge) */
    public void setKafkaLag(String topic, int partition, long lag) {
        Gauge.builder("seckill.kafka.lag", () -> lag)
                .description("Kafka 消费积压")
                .tag("topic", topic)
                .tag("partition", String.valueOf(partition))
                .register(meterRegistry);
    }

    /** Redis 与 DB 库存差异(对账告警用) */
    public void setConsistencyGap(String activityId, long gap) {
        Gauge.builder("seckill.consistency.gap", () -> gap)
                .description("Redis 与 DB 库存差异")
                .tag("activity_id", activityId)
                .register(meterRegistry);
    }

    /** 熔断器当前是否打开(0=闭合,1=打开;告警 CircuitBreakerOpen 引用) */
    public void setCircuitBreakerOpen(boolean open) {
        Gauge.builder("circuit_breaker_open", () -> open ? 1.0 : 0.0)
                .description("熔断器打开状态")
                .tag("job", "seckill")
                .register(meterRegistry);
    }
}
# application.yml - Prometheus 端点暴露(Spring Boot Actuator)
management:
  endpoints:
    web:
      exposure:
        include: prometheus,health,info
  metrics:
    tags:
      application: ${spring.application.name}
      env: ${ENV:local}
    export:
      prometheus:
        enabled: true
  endpoint:
    prometheus:
      enabled: true
Grafana 面板查询示例:

1. QPS 趋势(按活动和状态):
   sum by (activity_id, status) (rate(seckill_qps_total[1m]))

2. 成功率:
   rate(seckill_qps_total{status="success"}[1m]) / rate(seckill_qps_total[1m])

3. P99 延迟:
   histogram_quantile(0.99, sum(rate(seckill_latency_ms_bucket[1m])) by (le))

4. 库存消耗曲线:
   seckill_stock_remaining

5. Kafka 消费积压告警:
   seckill_kafka_lag > 10000

Maven 依赖:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

10.3 告警规则

# alert_rules.yml
groups:
  - name: seckill_alerts
    rules:
      # 高优:Redis 和 DB 库存不一致
      - alert: StockMismatch
        expr: abs(seckill_consistency_gap) > 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "活动{{ $labels.activity_id }}库存不一致,差异={{ $value }}"
          
      # 高优:MQ 消费积压严重
      - alert: KafkaLagHigh
        expr: seckill_kafka_lag > 10000
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "秒杀订单 MQ 积压超过 10000"
          
      # 高优:熔断器打开
      - alert: CircuitBreakerOpen
        expr: circuit_breaker_open{job="seckill"} == 1
        for: 0m
        labels:
          severity: critical
        annotations:
          summary: "秒杀链路熔断器已打开"
          
      # 中优:成功率下降
      - alert: HighFailureRate
        expr: (1 - seckill_success_count / (seckill_qps_total + 1)) > 0.5
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "秒杀失败率超过 50%"
          
      # 中优:P99 延迟高
      - alert: HighLatency
        expr: histogram_quantile(0.99, sum(rate(seckill_latency_ms_bucket[1m])) by (le)) > 500
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "秒杀链路 P99 延迟 > 500ms"

十一、容灾与高可用

前面的章节都在讨论「在正常情况下系统如何工作」,这一章要回答一个更残酷的问题:「当系统某一部分崩溃时,会怎样?」

这是架构设计中最容易被忽视、却是最关键的一环。很多秒杀系统在设计阶段表现完美,但到了大促当天因为某个意想不到的故障(网络分区、磁盘写满、DNS 缓存失效)而全面崩溃。根本原因是在设计时没有充分考虑「失败模式」。

在秒杀系统中,故障可能发生在任何层面:Redis 集群因热点 Key 过载而拒绝写入、数据库主从同步延迟导致从库读到脏数据、Kafka broker 磁盘满导致消息堆积、甚至机房级的电力故障导致整个数据中心宕机。对于每一种故障模式,我们都需要明确回答三个问题:

  1. 故障如何被检测到? 心跳检测?监控告警?业务指标异常?
  2. 故障对业务有什么影响? 是全链路阻断还是部分功能降级?
  3. 系统如何自动恢复? 是主备切换、灰度熔断、还是人工介入?

根据这些问题,我们将故障处理策略分为三个层次:多机房部署防机房级灾难、独立部署防连锁故障、降级预案防组件级故障。三个层次从大到小,覆盖了从全面灾难到局部故障的所有场景。

11.1 多机房部署

┌──────────────────────────────────────────────────────────────┐
│                   多机房容灾架构(三地五中心)                   │
│                                                              │
│          机房 A (华东)          机房 B (华南)                  │
│     ┌──────────────────┐   ┌──────────────────┐             │
│     │ LVS + Nginx      │   │ LVS + Nginx      │             │
│     │ API Gateway x10  │   │ API Gateway x10  │             │
│     │ Seckill-Svc x20  │   │ Seckill-Svc x20  │             │
│     │ Redis Master     │   │ Redis Slave      │             │
│     │ Kafka Broker x3  │   │ Kafka Broker x3  │             │
│     │ MySQL Master     │   │ MySQL Slave      │             │
│     └────────┬─────────┘   └────────┬─────────┘             │
│              │                      │                        │
│              └──────────┬───────────┘                        │
│                         │                                    │
│               ┌─────────▼──────────┐                         │
│               │ GSLB 全局调度       │                         │
│               │ (DNS + Anycast)     │                         │
│               └─────────┬──────────┘                         │
│                         │                                    │
│                    用户请求                                   │
│                                                              │
│  机房 C (华北 —— 备)                                          │
│     ┌──────────────────┐                                     │
│     │ Nginx x5         │                                     │
│     │ Redis Slave      │  (灾备,流量较小)                    │
│     │ MySQL Slave      │                                     │
│     └──────────────────┘                                     │
│                                                              │
│  流量调度策略:                                               │
│  1. 正常情况下 A:B = 6:4 流量分发                             │
│  2. 任一机房故障 → 流量全切到另一机房                          │
│  3. GSLB 自动切换时间 < 30s                                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

11.2 秒杀独立部署

核心原则:秒杀系统必须与其他业务系统物理隔离,避免秒杀流量打垮普通业务。

物理隔离方案:
                                                      
┌─────────────┐  ┌──────────────┐  ┌─────────────────┐
│  普通业务     │  │  秒杀独立集群  │  │  数据库独立      │
│  K8s 集群 A  │  │  K8s 集群 B  │  │  独立 MySQL 集群 │
│              │  │              │  │  独立 Redis 集群 │
│  商品查询     │  │  秒杀链路全栈  │  │  独立 Kafka 集群 │
│  用户中心     │  │  独立网关     │  │                 │
│  支付回调     │  │  独立服务     │  │  原因:           │
│  订单管理     │  │  独立缓存     │  │  防止缓存 key 冲突│
│  库存管理     │  │  独立 MQ     │  │  防止连接池耗尽    │
│  …           │  │              │  │  防止误杀          │
│              │  │  CPU/内存隔离 │  │                 │
│              │  │  网络隔离     │  │                 │
│              │  │  独立弹性伸缩  │  │                 │
└─────────────┘  └──────────────┘  └─────────────────┘

11.3 降级预案

降级等级:

L1(轻度降级)—— 触发条件:DB 写入延迟 > 100ms
  - 部分高级校验降级(如用户画像校验跳过)
  - 日志输出降级(只保留 ERROR 级别)
  - 效果:释放部分 CPU/DB 资源

L2(中度降级)—— 触发条件:DB 写入延迟 > 500ms 或错误率 > 10%
  - 关闭库存预校验(直接到 DB 兜底判断)
  - 结果通知延迟(发送结果从实时改为 3 秒轮询间隔)
  - 一些非核心统计暂停
  - 效果:保留核心链路,牺牲额外数据

L3(重度降级)—— 触发条件:DB 写入延迟 > 2s 或错误率 > 50%
  - 临时关闭秒杀入口(返回活动结束页面)
  - 仅保留库存扣减 + MQ 写入 + DB 处理的极简链路
  - 效果:确保不超卖,暂不接受新请求

L4(紧急降级)—— 触发条件:Redis 集群不可用
  - 完全关闭秒杀入口
  - 提示用户稍后重试
  - 效果:宁可停服也不超卖

降级自动化:
  - 由 Sentinel 自动触发熔断/限流
  - 关键阈值通过配置中心(Apollo/Nacos)动态调整
  - 每次降级触发记录日志 + 告警推送

十二、压测与容量规划

「纸上得来终觉浅,绝知此事要躬行」。一个秒杀系统在上线之前,压测是唯一能证明「这个系统真的能扛住百万 QPS」的手段。架构图画得再漂亮、代码写得再精妙,如果没有经过真实流量的洗礼,永远只是理论假设。

压测不仅仅是在测试「系统最大能跑多少 QPS」,更重要的是三个维度的评估:

  1. 瓶颈定位:通过逐级加压,找出系统的最短木板——是 CPU 先跑满、还是数据库连接池先耗尽、还是网络带宽先被打满?这个问题不能靠猜,必须靠实测。

  2. 降级验证:正常流量下熔断和降级逻辑不会被触发,只有通过超过设计容量的压测,才能验证降级策略是否真正在起作用、降级后系统是否依然稳定运行。

  3. 数据一致性验证:在极限并发下,超卖、少卖、重复下单的问题才可能暴露。压测期间需要对账 Job 持续比对 Redis 和 DB 的数据差异,确保在极端压力下核心指标依然达标。

容量规划是压测的前置工作——你需要先估算出需要的资源规模,然后构建压测环境去验证这个估算是否准确。资源估算的核心公式是:机器数 = 目标 QPS / 单机压测 QPS × 安全系数(通常 1.5~2)。安全系数之所以大于 1,是因为压测环境和线上环境存在差异:压测通常是简单的 Key-Value 操作,线上有更复杂的业务逻辑;压测是平稳流量,线上有突增波动;压测只有单个活动,线上可能有多个活动叠加。

12.1 压测方案

压测阶段:

阶段1:组件压测(完成时间:活动前 30 天)
────────────────────────────
  - Redis 单节点压测:测试 QPS 上限(单节点 10W+ ops/s)
  - MySQL 单表压测:测试 INSERT/UPDATE 上限
  - Kafka 单机压测:测试生产/消费吞吐
  - 网关单机压测:测试 QPS/CPU/内存曲线

阶段2:全链路压测(完成时间:活动前 14 天)
────────────────────────────
  - 使用影子库 / 影子表 / 影子 Topic
  - 压测流量标识传递:X-Pressure-Test: true
  - 阶梯加压:10% → 30% → 60% → 100% → 120%
  - 每次持续 30 分钟,观察系统状态
  - 找出各组件瓶颈并优化

阶段3:大促预演(完成时间:活动前 3 天)
────────────────────────────
  - 按活动当天真实流量模型压测
  - 模拟突发场景(Redis 故障、网络抖动、单机房宕机)
  - 验证降级预案有效性
  - 演练应急预案流程

压测关键指标:
┌──────────────────────────────────────────────────────────┐
│  指标                     │ 阈值          │ 说明          │
│─────────────────────────│──────────────│──────────────│
│  网关 P99 延迟             │ < 50ms       │ 不包含排队     │
│  网关错误率               │ < 0.1%       │ 5xx 计数      │
│  核心服务 P99 延迟         │ < 200ms      │ 从网关到返回   │
│  Redis 操作 P99            │ < 1ms        │ 包含网络       │
│  核心服务错误率            │ < 0.5%       │ 5xx 计数      │
│  MySQL 写入 TPS            │ > 5W tps     │ 单机上限       │
│  MySQL 慢查询              │ < 1%         │ 超过 100ms    │
│  MQ 消费延迟              │ < 5s         │ P99           │
│  CPU 使用率               │ < 70%        │ 安全水位       │
│  内存使用率               │ < 80%        │ 安全水位       │
│  网络带宽                 │ < 70%        │ 千兆网卡        │
│  连接池使用率             │ < 80%        │ MySQL 连接池   │
└──────────────────────────────────────────────────────────┘

12.2 容量规划

资源估算(支撑 100W 入口 QPS):

层                单机能力      所需机器数    备注
─────────────────────────────────────────────────────────
CDN峰值带宽                         云服务   不限      回源 1%
LVS (DR模式)       200W conn     2          Keepalived 主备
Nginx (7层)        5W QPS        20         按 80% 利用率
API Gateway        2W QPS        50         OpenResty, 已有限流
Seckill Service    5K QPS        20         Java(Spring Boot), 50并发
Redis Cluster      10W ops/s    6 Master   16GB/节点, 冗余 50%
Kafka Broker       40W msg/s     3          64分区, 3副本
Consumer Service   3K QPS        20         64分区并行消费
MySQL Master       5K TPS        1          库存写入
MySQL Slave        5K QPS        3          读 + 对账
数据库连接         3000          ProxySQL  读写分离
MQ 集群                                              3 副本
                                                    (跨机房)

弹性扩容策略:
  - 日常配置:30% 机器
  - 预热阶段(活动前30min):扩容到 80%
  - 活动进行中:HPA 自动扩缩 (CPU > 60% 扩容, < 30% 缩容)
  - 极端情况:预留 50% 资源用于手动扩容

12.3 大促 Checklist

┌──────────────────────────────────────────────────────────────┐
│                    大促上线 Checklist                         │
│                                                              │
│  □ 缓存预热活动库存到 Redis                                    │
│  □ Kafka topic 已创建且分区就绪                                │
│  □ 数据库分片已创建 & 连接池就绪                                │
│  □ 令牌池已初始化 (库存 × 放大系数)                             │
│  □ 活动信息已加载到 CDN 静态页面                                │
│  □ 限流规则已配置并验证                                       │
│  □ 熔断降级规则已配置                                         │
│  □ 监控面板已就绪 & 告警群已配置                               │
│  □ 核心链路幂等性已验证                                       │
│  □ 对账 Job 已配置好(每 5 分钟触发)                          │
│  □ 应急预案已演练通过                                         │
│  □ 回滚方案已就位(灰度可回滚服务代码)                          │
│  □ 压测报告已通过评审                                         │
│  □ 客服/运营培训完成                                          │
│  □ 前端页面倒计时同步到服务端时间(NTP 校准)                    │
│  □ 法务合规:活动规则审核 & 用户协议更新                        │
│  □ 财务系统:退款/对账接口就绪                                  │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│  大促中值班安排:                                              │
│  - 核心研发 on-call(响应时间 < 3min)                        │
│  - SRE 值班监控基础设施                                       │
│  - DBA 值班监控数据库                                          │
│  - 客服公关值班处理客诉                                        │
│  - 实时大屏监控:QPS/成功数/失败率/库存/延迟                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

附录

A. 核心参数配置速查

# 秒杀系统参数配置

gateway:
  global_limit: 100000        # 网关全局限流 QPS
  user_limit: 5               # 单用户限流 QPS
  ip_limit: 20                # 单 IP 限流 QPS
  queue_timeout_ms: 100       # 排队超时(毫秒)
  blacklist_ttl_s: 3600       # 黑名单有效期
  
redis:
  stock_ttl: 86400            # 库存 key TTL(24h)
  token_ttl: 600              # 用户 token TTL(10min)
  warmup_buffer: 1.5          # 预热缓冲系数
  pool_size: 100              # 连接池大小
  pool_timeout_ms: 50         # 连接池超时

mq:
  topic: seckill_orders
  partitions: 64              # 分区数
  replication: 3              # 副本数
  acks: all                   # 全副本确认
  retries: 3                  # 重试次数
  retry_interval_ms: 1000     # 重试间隔
  max_inflight: 1             # 保证有序

db:
  shard_count: 16             # 分库数
  table_per_shard: 32         # 每库分表数
  pool_size: 300              # 连接池大小
  max_idle_time_s: 300        # 连接空闲时间
  batch_insert_size: 100      # 批量插入大小 (如有)

retry:
  max_retries: 3              # 最大重试次数
  backoff_base_ms: 100       # 退避基数
  backoff_max_ms: 5000       # 退避上限

B. 常见面试题 / 技术难点速查

  1. Q: 为什么用 Redis Lua 而不是先查再扣?
    A: 因为 GET + DECR 不是原子操作,两个命令之间可能被其他命令插队导致超卖。Lua 脚本在 Redis 中原子执行。

  2. Q: Redis 库存和 DB 库存如何保证一致?
    A: 采用"预扣 + 异步落库 + 定期对账"策略。Redis 预扣是乐观的,DB 落库成功后才是最终确认,不一致时以对账 Job 修复。

  3. Q: 如果 Redis 挂了就超卖了吗?
    A: Redis 是核心防线之一,但不是唯一。Redis 挂了 DB 层有兜底保障(UPDATE WHERE 包含库存约束),但会损失可用性。因此 Redis 必须有集群高可用。

  4. Q: 如何支持商品分页(多种 SKU)的秒杀?
    A: 每个 SKU 作为独立的库存 key(seckill:stock:{activity_id}:{sku_id}),分布到不同的 Redis Slot 避免热点 Key。

  5. Q: 高 QPS 下 Go 如何减少 GC 压力?
    A: - 对象复用(sync.Pool)

    • 减少内存分配(用 []byte 池)
    • 使用 pprof 监控 GC 停顿
    • 避免高频创建 goroutine(用协程池)
  6. Q: 如何快速知道活动结束了?
    A: - Redis 库存归零后,在 Lua 中设置活动结束标志

    • 本地缓存 + 定期检查 Redis flag
    • 客户端接收 SSE 推送的 END 信号

版本: v1.0
最后更新: 2026-08-07
维护者: 架构组
适用场景: 百万 QPS 级别电商秒杀系统
设计周期: 2 周(含评审 + 压测 + 演练)