HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

02 - 秒杀系统设计

面试频率: 难度等级: 推荐时长: 45-60 分钟

目录

  • 需求分析与澄清
  • 容量估算
  • API 设计
  • 数据模型设计
  • 架构设计
  • 核心算法与实现
  • 优化方案
  • 监控告警
  • 面试问答

需求分析与澄清

业务场景

秒杀系统是电商平台的核心业务场景之一,特点是短时间内大量用户抢购有限库存商品。典型场景包括:

  • 双11/618 大促秒杀
  • 品牌新品首发
  • 限量款商品抢购
  • 优惠券秒杀

核心挑战

秒杀系统的三大挑战:
┌─────────────────────────────────────────────────────────┐
│ 1. 高并发流量                                            │
│    - 瞬时流量是平时的 100-1000 倍                        │
│    - 大量无效请求(库存已空仍在请求)                     │
│                                                          │
│ 2. 超卖问题                                              │
│    - 库存扣减的原子性保证                                │
│    - 分布式环境下的数据一致性                            │
│                                                          │
│ 3. 恶意攻击                                              │
│    - 黄牛刷单、机器人攻击                                │
│    - 接口重放攻击                                        │
└─────────────────────────────────────────────────────────┘

功能性需求

面试官视角的关键问题

在面试开始时,面试官通常会问:

面试官: "设计一个秒杀系统,支持高并发抢购场景。"

你需要主动澄清以下需求:

Q1: 秒杀规模是多大?

用户规模:1000 万日活用户
秒杀参与率:约 10%(100 万用户参与秒杀)
峰值 QPS:10 万 QPS(开始前 5 秒)
商品库存:通常 100-10000 件

Q2: 需要支持哪些核心功能?

 秒杀前:商品预热、倒计时展示
 秒杀中:下单、库存扣减、支付
 秒杀后:订单查询、超时取消
 不需要:复杂的优惠券叠加、积分抵扣

Q3: 对一致性的要求?

强一致性:库存扣减(不能超卖)
最终一致性:订单状态同步、积分返还
可用性优先:商品详情查询、倒计时展示

Q4: 性能目标?

响应时间:< 500ms(P99)
成功率:> 99.9%(排除限流)
超卖率:0(绝对不能超卖)

非功能性需求

需求类型具体要求优先级
高可用99.99% 可用性,单点故障自动切换P0
高并发支持 10 万 QPS 峰值流量P0
数据一致性库存扣减强一致性,订单最终一致性P0
安全性防刷、防重放、防黄牛P0
可扩展性水平扩展支持更大规模P1
可观测性实时监控、告警、链路追踪P1

容量估算

流量估算

日常流量 vs 秒杀流量

【日常流量】
DAU: 1000 万
日均订单: 200 万单
QPS: 200万 / (24 * 3600) ≈ 23 QPS
峰值 QPS: 23 * 10 = 230 QPS

【秒杀流量】
参与用户: 100 万
秒杀时长: 5 秒
峰值 QPS: 100万 / 5 = 20 万 QPS

流量放大倍数 = 20万 / 230 ≈ 870 倍

各接口 QPS 分布

接口类型            QPS      占比    说明
─────────────────────────────────────────────
商品详情查询        15 万    75%     大量用户刷新页面
秒杀下单请求        4 万     20%     实际下单请求
订单查询            1 万     5%      查询抢购结果
─────────────────────────────────────────────
总计                20 万    100%

存储估算

核心数据表容量

秒杀商品表

单条记录: 1 KB
秒杀商品数: 1000 个(同时进行)
总存储: 1000 * 1KB = 1 MB(可忽略)

秒杀订单表

单条记录: 2 KB(包含用户信息、商品信息、时间戳等)
每场秒杀订单数: 10000 单
每日秒杀场次: 100 场
每日订单量: 100 * 10000 = 100 万单
每日存储增长: 100万 * 2KB = 2 GB

一年存储: 2GB * 365 = 730 GB ≈ 0.73 TB
三年存储: 0.73 * 3 ≈ 2.2 TB(冷热分离)

库存流水表(用于对账)

单条记录: 500 Bytes
每日操作数: 100 万次
每日存储: 100万 * 500B = 500 MB

一年存储: 500MB * 365 ≈ 180 GB

缓存容量

Redis 缓存

商品详情缓存:
  - 1000 个秒杀商品 * 5KB = 5 MB

库存缓存:
  - 1000 个商品 * 100B = 100 KB

用户购买记录(去重):
  - 100 万用户 * 200B = 200 MB

总计: 约 500 MB(考虑冗余)
实际分配: 2 GB(预留扩展空间)

带宽估算

【下行带宽】(服务器 → 用户)
商品详情请求: 15 万 QPS * 5 KB = 750 MB/s = 6 Gbps
订单结果响应: 4 万 QPS * 1 KB = 40 MB/s = 0.32 Gbps
总计: ≈ 7 Gbps

【上行带宽】(用户 → 服务器)
秒杀请求: 4 万 QPS * 500 Bytes = 20 MB/s = 0.16 Gbps
商品详情请求: 15 万 QPS * 200 Bytes = 30 MB/s = 0.24 Gbps
总计: ≈ 0.5 Gbps

建议带宽: 10 Gbps(考虑 1.5 倍冗余)

服务器估算

【应用服务器】
单机 QPS: 1000(假设)
峰值 QPS: 20 万
所需服务器: 20万 / 1000 = 200 台
实际部署: 300 台(1.5 倍冗余)

【数据库】
主库: 1 台(16C 64G)
从库: 4 台(读写分离,读多写少)
分片: 8 个分片(按用户 ID 分片)

【缓存】
Redis 集群: 3 主 3 从(哨兵模式)
单节点: 8C 32G
总内存: 32GB * 6 = 192 GB

成本估算(云服务)

【计算成本】
应用服务器: 300 台 * 4C8G * $0.15/小时 * 24 * 30 = $32,400/月
数据库: 5 台 * 16C64G * $0.6/小时 * 24 * 30 = $21,600/月
Redis: 6 台 * 8C32G * $0.3/小时 * 24 * 30 = $12,960/月

【存储成本】
数据库存储: 3 TB * $0.1/GB/月 = $300/月
对象存储: 5 TB * $0.02/GB/月 = $100/月

【带宽成本】
10 Gbps * $50/Mbps/月 = $500,000/月

总计: 约 $567,360/月(主要成本在带宽)

优化方案: 使用 CDN 降低带宽成本至 $50,000/月

API 设计

RESTful API 设计

1. 秒杀商品相关

# 获取秒杀商品列表
GET /api/v1/seckill/products?status={upcoming|ongoing|ended}&page=1&size=20

Response 200:
{
  "code": 0,
  "data": {
    "items": [
      {
        "product_id": 123456,
        "title": "iPhone 15 Pro Max 256G",
        "original_price": 9999,
        "seckill_price": 7999,
        "stock": 1000,
        "start_time": "2025-11-12T20:00:00Z",
        "end_time": "2025-11-12T20:05:00Z",
        "status": "upcoming"  // upcoming, ongoing, ended
      }
    ],
    "total": 100,
    "page": 1,
    "size": 20
  }
}

# 获取秒杀商品详情
GET /api/v1/seckill/products/{product_id}

Response 200:
{
  "code": 0,
  "data": {
    "product_id": 123456,
    "title": "iPhone 15 Pro Max 256G",
    "description": "...",
    "images": ["url1", "url2"],
    "original_price": 9999,
    "seckill_price": 7999,
    "stock": 1000,              // 剩余库存(可能延迟)
    "start_time": "2025-11-12T20:00:00Z",
    "end_time": "2025-11-12T20:05:00Z",
    "user_limit": 1,            // 每人限购数量
    "status": "ongoing",
    "server_time": "2025-11-12T20:00:05Z"  // 服务器时间(防止客户端时间不准)
  }
}

2. 秒杀下单

# 下单请求
POST /api/v1/seckill/orders

Headers:
  Authorization: Bearer {token}
  X-Request-ID: {unique_request_id}  // 防重放

Request Body:
{
  "product_id": 123456,
  "quantity": 1,
  "token": "seckill_token_xxx"  // 秒杀令牌(预先获取)
}

Response 200 (成功):
{
  "code": 0,
  "message": "秒杀成功",
  "data": {
    "order_id": "SK202511120001",
    "product_id": 123456,
    "quantity": 1,
    "total_price": 7999,
    "pay_expire_time": "2025-11-12T20:15:00Z"  // 支付超时时间
  }
}

Response 429 (限流):
{
  "code": 429,
  "message": "请求过于频繁,请稍后再试",
  "data": {
    "retry_after": 1  // 秒
  }
}

Response 400 (库存不足):
{
  "code": 40001,
  "message": "商品已售罄",
  "data": null
}

Response 400 (重复购买):
{
  "code": 40002,
  "message": "您已购买过该商品",
  "data": null
}

Response 400 (未开始/已结束):
{
  "code": 40003,
  "message": "秒杀未开始或已结束",
  "data": null
}

3. 秒杀令牌获取(防刷)

# 获取秒杀令牌
POST /api/v1/seckill/token

Headers:
  Authorization: Bearer {token}

Request Body:
{
  "product_id": 123456
}

Response 200:
{
  "code": 0,
  "data": {
    "token": "seckill_token_xxx",
    "expire_time": "2025-11-12T20:00:10Z"  // 令牌有效期 10 秒
  }
}

【说明】
- 令牌在秒杀开始前 1 分钟可获取
- 令牌包含用户 ID、商品 ID、时间戳、签名
- 一次性使用,防止重放攻击

4. 订单查询

# 查询我的秒杀订单
GET /api/v1/seckill/orders/my?status={pending|paid|cancelled}&page=1&size=20

Response 200:
{
  "code": 0,
  "data": {
    "items": [
      {
        "order_id": "SK202511120001",
        "product_id": 123456,
        "title": "iPhone 15 Pro Max",
        "quantity": 1,
        "total_price": 7999,
        "status": "pending",  // pending, paid, cancelled, timeout
        "created_at": "2025-11-12T20:00:05Z",
        "pay_expire_time": "2025-11-12T20:15:00Z"
      }
    ],
    "total": 5,
    "page": 1,
    "size": 20
  }
}

# 查询订单详情
GET /api/v1/seckill/orders/{order_id}

Response 200:
{
  "code": 0,
  "data": {
    "order_id": "SK202511120001",
    "product_id": 123456,
    "product_snapshot": { /* 商品快照 */ },
    "quantity": 1,
    "total_price": 7999,
    "status": "pending",
    "created_at": "2025-11-12T20:00:05Z",
    "paid_at": null,
    "pay_expire_time": "2025-11-12T20:15:00Z"
  }
}

5. 订单支付(简化)

# 支付订单
POST /api/v1/seckill/orders/{order_id}/pay

Request Body:
{
  "payment_method": "alipay",  // alipay, wechat
  "return_url": "https://example.com/callback"
}

Response 200:
{
  "code": 0,
  "data": {
    "payment_url": "https://pay.alipay.com/...",  // 跳转到支付页面
    "order_id": "SK202511120001"
  }
}

API 错误码设计

// 错误码定义
const (
    // 成功
    CodeSuccess = 0

    // 客户端错误 4xxxx
    CodeInvalidParam      = 40000  // 参数错误
    CodeStockNotEnough    = 40001  // 库存不足
    CodeDuplicatePurchase = 40002  // 重复购买
    CodeSeckillNotStart   = 40003  // 秒杀未开始
    CodeSeckillEnded      = 40004  // 秒杀已结束
    CodeInvalidToken      = 40005  // 令牌无效
    CodeUserLimitExceed   = 40006  // 超过限购数量

    // 限流/熔断 429xx
    CodeRateLimitExceeded = 42900  // 限流
    CodeCircuitBreakerOpen = 42901 // 熔断

    // 服务端错误 5xxxx
    CodeInternalError     = 50000  // 内部错误
    CodeServiceUnavailable = 50300 // 服务不可用
    CodeDatabaseError     = 50001  // 数据库错误
    CodeCacheError        = 50002  // 缓存错误
)

数据模型设计

核心表结构

1. 秒杀商品表 (seckill_product)

CREATE TABLE seckill_product (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    product_id BIGINT UNSIGNED NOT NULL COMMENT '商品 ID(关联商品表)',
    title VARCHAR(255) NOT NULL COMMENT '商品标题',
    original_price INT UNSIGNED NOT NULL COMMENT '原价(分)',
    seckill_price INT UNSIGNED NOT NULL COMMENT '秒杀价(分)',
    stock INT UNSIGNED NOT NULL COMMENT '总库存',
    available_stock INT UNSIGNED NOT NULL COMMENT '可用库存',
    user_limit TINYINT UNSIGNED DEFAULT 1 COMMENT '每人限购数量',
    start_time TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
    end_time TIMESTAMP NOT NULL COMMENT '秒杀结束时间',
    status TINYINT UNSIGNED DEFAULT 0 COMMENT '状态: 0-未开始 1-进行中 2-已结束',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_product_id (product_id),
    INDEX idx_start_time (start_time),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';

-- 示例数据
INSERT INTO seckill_product VALUES
(1, 123456, 'iPhone 15 Pro Max 256G', 999900, 799900, 1000, 1000, 1,
 '2025-11-12 20:00:00', '2025-11-12 20:05:00', 0, NOW(), NOW());

2. 秒杀订单表 (seckill_order)

CREATE TABLE seckill_order (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    order_id VARCHAR(32) NOT NULL COMMENT '订单号',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
    product_id BIGINT UNSIGNED NOT NULL COMMENT '商品 ID',
    seckill_id BIGINT UNSIGNED NOT NULL COMMENT '秒杀活动 ID',
    quantity TINYINT UNSIGNED NOT NULL COMMENT '购买数量',
    total_price INT UNSIGNED NOT NULL COMMENT '总价(分)',
    status TINYINT UNSIGNED DEFAULT 0 COMMENT '状态: 0-待支付 1-已支付 2-已取消 3-超时取消',
    pay_expire_time TIMESTAMP NOT NULL COMMENT '支付超时时间',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    paid_at TIMESTAMP NULL COMMENT '支付时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    UNIQUE KEY uk_order_id (order_id),
    INDEX idx_user_product (user_id, product_id) COMMENT '防重复购买',
    INDEX idx_user_id (user_id),
    INDEX idx_seckill_id (seckill_id),
    INDEX idx_status (status),
    INDEX idx_pay_expire (pay_expire_time) COMMENT '定时任务扫描超时订单'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

-- 分表策略(可选)
-- 按 user_id 分 8 张表: seckill_order_0 ~ seckill_order_7

3. 库存流水表 (stock_log)

CREATE TABLE stock_log (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    seckill_id BIGINT UNSIGNED NOT NULL COMMENT '秒杀活动 ID',
    order_id VARCHAR(32) NOT NULL COMMENT '订单号',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
    quantity INT NOT NULL COMMENT '库存变化量(正数-增加,负数-扣减)',
    stock_before INT UNSIGNED NOT NULL COMMENT '变更前库存',
    stock_after INT UNSIGNED NOT NULL COMMENT '变更后库存',
    operation_type TINYINT UNSIGNED NOT NULL COMMENT '操作类型: 1-下单扣减 2-取消回退',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_seckill_id (seckill_id),
    INDEX idx_order_id (order_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表(用于对账)';

4. 用户购买记录表 (user_purchase)

CREATE TABLE user_purchase (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
    user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
    seckill_id BIGINT UNSIGNED NOT NULL COMMENT '秒杀活动 ID',
    product_id BIGINT UNSIGNED NOT NULL COMMENT '商品 ID',
    quantity TINYINT UNSIGNED NOT NULL COMMENT '购买数量',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY uk_user_seckill (user_id, seckill_id) COMMENT '防重复购买',
    INDEX idx_user_id (user_id),
    INDEX idx_seckill_id (seckill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户购买记录(去重)';

Redis 数据结构设计

1. 库存缓存

【String 类型】
Key: seckill:stock:{seckill_id}
Value: 可用库存数量
TTL: 秒杀结束后 1 小时

示例:
SET seckill:stock:1 1000
GET seckill:stock:1

# Lua 脚本原子扣减库存
local key = KEYS[1]
local quantity = tonumber(ARGV[1])
local stock = tonumber(redis.call('GET', key) or 0)

if stock >= quantity then
    redis.call('DECRBY', key, quantity)
    return stock - quantity  -- 返回剩余库存
else
    return -1  -- 库存不足
end

2. 用户购买记录(去重)

【Set 类型】
Key: seckill:users:{seckill_id}
Value: user_id 集合
TTL: 秒杀结束后 1 天

示例:
SADD seckill:users:1 10001 10002 10003
SISMEMBER seckill:users:1 10001  # 返回 1(已购买)
SISMEMBER seckill:users:1 99999  # 返回 0(未购买)

3. 秒杀商品信息缓存

【Hash 类型】
Key: seckill:product:{seckill_id}
Value: 商品详情
TTL: 秒杀结束后 1 小时

示例:
HSET seckill:product:1 title "iPhone 15 Pro" price 799900 stock 1000
HGETALL seckill:product:1

4. 秒杀令牌

【String 类型】
Key: seckill:token:{user_id}:{seckill_id}
Value: token 值(加密)
TTL: 10 秒

示例:
SET seckill:token:10001:1 "encrypted_token_xxx" EX 10
GET seckill:token:10001:1

5. 限流计数器

【String 类型】
Key: rate_limit:user:{user_id}
Value: 请求次数
TTL: 1 秒

示例(滑动窗口限流):
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = tonumber(redis.call('GET', key) or 0)
if current >= limit then
    return 0  -- 超过限流
else
    redis.call('INCR', key)
    if current == 0 then
        redis.call('EXPIRE', key, window)
    end
    return 1  -- 通过限流
end

数据一致性保证

缓存与数据库一致性

【策略选择】
1. Cache-Aside (旁路缓存)
   - 写:先更新 DB,再删除 Cache
   - 读:先查 Cache,Miss 则查 DB 并回写

2. 延迟双删
   - 更新 DB 前删除 Cache
   - 更新 DB
   - 延迟 500ms 后再删除 Cache(防止脏读)

3. 订阅 Binlog
   - 使用 Canal 监听 MySQL Binlog
   - 异步更新 Redis 缓存
   - 解耦,但延迟稍高

【秒杀场景选择】
采用 Cache-Aside + 延迟双删
- 库存数据从 DB 加载到 Redis
- 扣减时先扣 Redis,成功后异步更新 DB
- DB 更新失败时回滚 Redis(补偿机制)

架构设计

V1 版本:单体架构(MVP)

架构图

用户请求
    ↓
┌────────────────────────────────────────┐
│         Nginx 负载均衡                  │
└────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────┐
│      秒杀服务(单体应用)               │
│  ┌──────────────────────────────────┐  │
│  │  接口层                          │  │
│  │  - 商品查询                      │  │
│  │  - 秒杀下单                      │  │
│  │  - 订单查询                      │  │
│  └──────────────────────────────────┘  │
│  ┌──────────────────────────────────┐  │
│  │  业务层                          │  │
│  │  - 库存校验                      │  │
│  │  - 订单创建                      │  │
│  │  - 支付对接                      │  │
│  └──────────────────────────────────┘  │
│  ┌──────────────────────────────────┐  │
│  │  数据访问层                      │  │
│  │  - MySQL 操作                    │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────┐
│         MySQL 主从                      │
│      Master (写)  +  Slave (读)         │
└────────────────────────────────────────┘

核心代码实现

库存扣减(悲观锁)

package service

import (
    "database/sql"
    "errors"
)

type SeckillService struct {
    db *sql.DB
}

// CreateOrder 创建秒杀订单(V1 版本 - 悲观锁)
func (s *SeckillService) CreateOrder(userID, seckillID int64, quantity int) (string, error) {
    // 开启事务
    tx, err := s.db.Begin()
    if err != nil {
        return "", err
    }
    defer tx.Rollback()

    // 1. 检查用户是否已购买(防重复)
    var count int
    checkSQL := "SELECT COUNT(*) FROM user_purchase WHERE user_id = ? AND seckill_id = ?"
    if err := tx.QueryRow(checkSQL, userID, seckillID).Scan(&count); err != nil {
        return "", err
    }
    if count > 0 {
        return "", errors.New("您已购买过该商品")
    }

    // 2. 锁定库存并扣减(悲观锁 FOR UPDATE)
    var stock, productID int64
    var price int
    lockSQL := `
        SELECT product_id, available_stock, seckill_price
        FROM seckill_product
        WHERE id = ? AND status = 1
        FOR UPDATE
    `
    if err := tx.QueryRow(lockSQL, seckillID).Scan(&productID, &stock, &price); err != nil {
        return "", err
    }

    if stock < quantity {
        return "", errors.New("库存不足")
    }

    // 扣减库存
    updateSQL := "UPDATE seckill_product SET available_stock = available_stock - ? WHERE id = ?"
    if _, err := tx.Exec(updateSQL, quantity, seckillID); err != nil {
        return "", err
    }

    // 3. 创建订单
    orderID := generateOrderID()
    insertOrderSQL := `
        INSERT INTO seckill_order (order_id, user_id, product_id, seckill_id, quantity, total_price, status, pay_expire_time)
        VALUES (?, ?, ?, ?, ?, ?, 0, DATE_ADD(NOW(), INTERVAL 15 MINUTE))
    `
    if _, err := tx.Exec(insertOrderSQL, orderID, userID, productID, seckillID, quantity, price*quantity); err != nil {
        return "", err
    }

    // 4. 记录用户购买
    insertPurchaseSQL := "INSERT INTO user_purchase (user_id, seckill_id, product_id, quantity) VALUES (?, ?, ?, ?)"
    if _, err := tx.Exec(insertPurchaseSQL, userID, seckillID, productID, quantity); err != nil {
        return "", err
    }

    // 5. 记录库存流水
    insertLogSQL := `
        INSERT INTO stock_log (seckill_id, order_id, user_id, quantity, stock_before, stock_after, operation_type)
        VALUES (?, ?, ?, ?, ?, ?, 1)
    `
    if _, err := tx.Exec(insertLogSQL, seckillID, orderID, userID, -quantity, stock, stock-quantity); err != nil {
        return "", err
    }

    // 提交事务
    if err := tx.Commit(); err != nil {
        return "", err
    }

    return orderID, nil
}

func generateOrderID() string {
    // 生成订单号: SK + 时间戳 + 随机数
    return fmt.Sprintf("SK%d%04d", time.Now().Unix(), rand.Intn(10000))
}

V1 架构的问题

问题分析:
┌─────────────────────────────────────────────┐
│ 1. 性能瓶颈                                  │
│    - 数据库连接数有限(MySQL 最大 1000)     │
│    - 悲观锁导致大量线程阻塞                  │
│    - 单点 TPS: 约 1000(远低于 10 万 QPS)  │
│                                              │
│ 2. 并发问题                                  │
│    - 大量请求直接打到 DB                     │
│    - FOR UPDATE 锁竞争激烈                   │
│    - 事务长时间占用连接                      │
│                                              │
│ 3. 可用性问题                                │
│    - DB 单点故障风险                         │
│    - 无限流保护                              │
│    - 无熔断降级                              │
└─────────────────────────────────────────────┘

V2 版本:缓存 + 限流优化

架构图

用户请求
    ↓
┌────────────────────────────────────────────────┐
│              CDN(静态资源)                    │
└────────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────────┐
│         Nginx + 接入层限流                      │
│         (限流: 1000 QPS/IP)                    │
└────────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────────┐
│          秒杀服务集群(20 台)                  │
│  ┌──────────────────────────────────────────┐  │
│  │  ① 用户维度限流 (10 req/s)               │  │
│  │  ② 令牌桶验证                            │  │
│  │  ③ Redis 预扣库存                        │  │
│  │  ④ 异步写入 DB                           │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘
         ↓                     ↓
    ┌─────────┐          ┌─────────────┐
    │  Redis  │          │  Kafka MQ   │
    │  集群   │          │  削峰填谷   │
    └─────────┘          └─────────────┘
         ↓                     ↓
┌────────────────────────────────────────────────┐
│           MySQL 主从 + 分库分表                 │
└────────────────────────────────────────────────┘

核心改进点

1. 多级限流

package middleware

import (
    "golang.org/x/time/rate"
    "sync"
)

// 用户维度限流器
type UserRateLimiter struct {
    limiters map[int64]*rate.Limiter
    mu       sync.RWMutex
    rate     rate.Limit  // 每秒请求数
    burst    int         // 桶容量
}

func NewUserRateLimiter(r rate.Limit, b int) *UserRateLimiter {
    return &UserRateLimiter{
        limiters: make(map[int64]*rate.Limiter),
        rate:     r,
        burst:    b,
    }
}

func (u *UserRateLimiter) GetLimiter(userID int64) *rate.Limiter {
    u.mu.Lock()
    defer u.mu.Unlock()

    limiter, exists := u.limiters[userID]
    if !exists {
        limiter = rate.NewLimiter(u.rate, u.burst)
        u.limiters[userID] = limiter
    }

    return limiter
}

func (u *UserRateLimiter) Allow(userID int64) bool {
    return u.GetLimiter(userID).Allow()
}

// HTTP 中间件
func RateLimitMiddleware(limiter *UserRateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID := getUserIDFromContext(c)

        if !limiter.Allow(userID) {
            c.JSON(429, gin.H{
                "code":    42900,
                "message": "请求过于频繁,请稍后再试",
            })
            c.Abort()
            return
        }

        c.Next()
    }
}

2. Redis 预扣库存

package service

import (
    "context"
    "github.com/go-redis/redis/v8"
)

type SeckillServiceV2 struct {
    rdb   *redis.Client
    db    *sql.DB
    kafka *kafka.Producer
}

// Lua 脚本:原子扣减库存
const decrStockScript = `
local key = KEYS[1]
local userKey = KEYS[2]
local quantity = tonumber(ARGV[1])
local userID = ARGV[2]

-- 检查用户是否已购买
if redis.call('SISMEMBER', userKey, userID) == 1 then
    return -2  -- 重复购买
end

-- 扣减库存
local stock = tonumber(redis.call('GET', key) or 0)
if stock >= quantity then
    redis.call('DECRBY', key, quantity)
    redis.call('SADD', userKey, userID)
    return stock - quantity  -- 返回剩余库存
else
    return -1  -- 库存不足
end
`

func (s *SeckillServiceV2) CreateOrder(ctx context.Context, userID, seckillID int64, quantity int) (string, error) {
    stockKey := fmt.Sprintf("seckill:stock:%d", seckillID)
    userKey := fmt.Sprintf("seckill:users:%d", seckillID)

    // 执行 Lua 脚本预扣库存
    result, err := s.rdb.Eval(ctx, decrStockScript,
        []string{stockKey, userKey},
        quantity, userID).Int()
    if err != nil {
        return "", err
    }

    if result == -2 {
        return "", errors.New("您已购买过该商品")
    }
    if result == -1 {
        return "", errors.New("库存不足")
    }

    // 生成订单 ID
    orderID := generateOrderID()

    // 异步发送到 Kafka(削峰)
    orderMsg := OrderMessage{
        OrderID:   orderID,
        UserID:    userID,
        SeckillID: seckillID,
        Quantity:  quantity,
        Timestamp: time.Now().Unix(),
    }

    if err := s.kafka.Publish("seckill-orders", orderMsg); err != nil {
        // 发送失败,回滚 Redis 库存
        s.rollbackRedisStock(ctx, stockKey, userKey, userID, quantity)
        return "", err
    }

    return orderID, nil
}

// 回滚 Redis 库存
func (s *SeckillServiceV2) rollbackRedisStock(ctx context.Context, stockKey, userKey string, userID int64, quantity int) {
    s.rdb.IncrBy(ctx, stockKey, int64(quantity))
    s.rdb.SRem(ctx, userKey, userID)
}

3. Kafka 消费者异步入库

package consumer

import (
    "github.com/Shopify/sarama"
)

type OrderConsumer struct {
    db *sql.DB
}

func (c *OrderConsumer) Consume(msg *sarama.ConsumerMessage) error {
    var order OrderMessage
    if err := json.Unmarshal(msg.Value, &order); err != nil {
        return err
    }

    // 开启事务
    tx, err := c.db.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // 1. 查询商品信息
    var productID int64
    var price int
    querySQL := "SELECT product_id, seckill_price FROM seckill_product WHERE id = ?"
    if err := tx.QueryRow(querySQL, order.SeckillID).Scan(&productID, &price); err != nil {
        return err
    }

    // 2. 插入订单
    insertOrderSQL := `
        INSERT INTO seckill_order (order_id, user_id, product_id, seckill_id, quantity, total_price, status, pay_expire_time)
        VALUES (?, ?, ?, ?, ?, ?, 0, DATE_ADD(NOW(), INTERVAL 15 MINUTE))
    `
    if _, err := tx.Exec(insertOrderSQL, order.OrderID, order.UserID, productID, order.SeckillID, order.Quantity, price*order.Quantity); err != nil {
        return err
    }

    // 3. 扣减 DB 库存(最终一致性)
    updateSQL := "UPDATE seckill_product SET available_stock = available_stock - ? WHERE id = ? AND available_stock >= ?"
    result, err := tx.Exec(updateSQL, order.Quantity, order.SeckillID, order.Quantity)
    if err != nil {
        return err
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        // DB 库存不足(Redis 和 DB 不一致),需要告警
        log.Error("DB stock insufficient", "seckill_id", order.SeckillID)
        return errors.New("DB 库存不足")
    }

    // 4. 记录用户购买
    insertPurchaseSQL := "INSERT INTO user_purchase (user_id, seckill_id, product_id, quantity) VALUES (?, ?, ?, ?)"
    if _, err := tx.Exec(insertPurchaseSQL, order.UserID, order.SeckillID, productID, order.Quantity); err != nil {
        return err
    }

    // 5. 记录库存流水
    insertLogSQL := `
        INSERT INTO stock_log (seckill_id, order_id, user_id, quantity, stock_before, stock_after, operation_type)
        VALUES (?, ?, ?, ?, 0, 0, 1)
    `
    if _, err := tx.Exec(insertLogSQL, order.SeckillID, order.OrderID, order.UserID, -order.Quantity); err != nil {
        return err
    }

    // 提交事务
    return tx.Commit()
}

V2 性能提升

性能对比:
┌────────────────────────────────────────────────┐
│ 指标              V1 版本      V2 版本         │
├────────────────────────────────────────────────┤
│ 峰值 TPS          1,000       50,000          │
│ 响应时间 P99      5000ms      100ms           │
│ DB 连接数         1000        100             │
│ 库存扣减延迟      实时        ~500ms (异步)    │
│ 超卖风险          低          极低(Lua 原子) │
└────────────────────────────────────────────────┘

优化效果:
 TPS 提升 50 倍(Redis 内存操作)
 响应时间降低 98%
 DB 压力降低 90%(Kafka 削峰)
 支持水平扩展(无状态服务)

V3 版本:高可用 + 全链路优化

架构图

                         用户请求
                             ↓
        ┌────────────────────────────────────────┐
        │         CDN (静态资源 + 页面缓存)       │
        └────────────────────────────────────────┘
                             ↓
        ┌────────────────────────────────────────┐
        │       WAF (防刷 + DDoS 防护)            │
        └────────────────────────────────────────┘
                             ↓
        ┌────────────────────────────────────────┐
        │    API Gateway (认证 + 限流 + 熔断)     │
        │    - 全局限流: 10 万 QPS                │
        │    - 用户限流: 10 req/s                 │
        │    - IP 限流: 1000 req/s                │
        └────────────────────────────────────────┘
                             ↓
        ┌────────────────────────────────────────┐
        │         负载均衡 (LVS + Nginx)          │
        └────────────────────────────────────────┘
                             ↓
    ┌───────────┬───────────┬───────────┬──────────┐
    │   Region  │   Region  │   Region  │  Region  │
    │   华北    │   华东    │   华南    │  西南    │
    └───────────┴───────────┴───────────┴──────────┘
                             ↓
        ┌────────────────────────────────────────┐
        │      秒杀服务集群 (300 台,多可用区)    │
        │  ┌──────────────────────────────────┐  │
        │  │  - 令牌验证                      │  │
        │  │  - Redis 预扣库存                │  │
        │  │  - 消息队列异步                  │  │
        │  │  - 熔断降级                      │  │
        │  └──────────────────────────────────┘  │
        └────────────────────────────────────────┘
              ↓                           ↓
    ┌──────────────────┐       ┌──────────────────┐
    │   Redis 集群     │       │   Kafka 集群     │
    │   (哨兵 + 分片)   │       │   (3 副本)       │
    │   - 主从: 3M3S   │       │   - 削峰填谷     │
    │   - 分片: 16     │       │   - 顺序消费     │
    └──────────────────┘       └──────────────────┘
                                      ↓
                         ┌──────────────────────┐
                         │  订单消费者集群       │
                         │  (100 台)            │
                         └──────────────────────┘
                                      ↓
        ┌────────────────────────────────────────┐
        │     MySQL 分库分表 (8 库 * 8 表)        │
        │     - 主从: 每库 1M + 2S                │
        │     - 分片键: user_id                   │
        └────────────────────────────────────────┘
                             ↓
        ┌────────────────────────────────────────┐
        │     监控告警 (Prometheus + Grafana)     │
        │     - 实时监控                          │
        │     - 链路追踪                          │
        │     - 日志聚合                          │
        └────────────────────────────────────────┘

核心优化点

1. 令牌桶算法生成秒杀令牌

package service

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "fmt"
    "time"
)

type TokenService struct {
    secretKey string
}

// GenerateToken 生成秒杀令牌
func (t *TokenService) GenerateToken(userID, seckillID int64) (string, error) {
    // 令牌有效期 10 秒
    expireTime := time.Now().Add(10 * time.Second).Unix()

    // 生成签名: HMAC-SHA256(userID|seckillID|expireTime, secretKey)
    data := fmt.Sprintf("%d|%d|%d", userID, seckillID, expireTime)
    h := hmac.New(sha256.New, []byte(t.secretKey))
    h.Write([]byte(data))
    signature := base64.URLEncoding.EncodeToString(h.Sum(nil))

    // 令牌格式: userID|seckillID|expireTime|signature
    token := fmt.Sprintf("%s|%s", data, signature)
    return base64.URLEncoding.EncodeToString([]byte(token)), nil
}

// VerifyToken 验证秒杀令牌
func (t *TokenService) VerifyToken(token string, userID, seckillID int64) error {
    // 解码
    decoded, err := base64.URLEncoding.DecodeString(token)
    if err != nil {
        return errors.New("令牌格式错误")
    }

    // 解析
    parts := strings.Split(string(decoded), "|")
    if len(parts) != 4 {
        return errors.New("令牌格式错误")
    }

    tokenUserID, _ := strconv.ParseInt(parts[0], 10, 64)
    tokenSeckillID, _ := strconv.ParseInt(parts[1], 10, 64)
    expireTime, _ := strconv.ParseInt(parts[2], 10, 64)
    signature := parts[3]

    // 验证用户和商品
    if tokenUserID != userID || tokenSeckillID != seckillID {
        return errors.New("令牌无效")
    }

    // 验证过期时间
    if time.Now().Unix() > expireTime {
        return errors.New("令牌已过期")
    }

    // 验证签名
    data := fmt.Sprintf("%d|%d|%d", userID, seckillID, expireTime)
    h := hmac.New(sha256.New, []byte(t.secretKey))
    h.Write([]byte(data))
    expectedSignature := base64.URLEncoding.EncodeToString(h.Sum(nil))

    if signature != expectedSignature {
        return errors.New("令牌签名错误")
    }

    return nil
}

2. 熔断降级

package middleware

import (
    "github.com/sony/gobreaker"
    "time"
)

// 熔断器配置
func NewCircuitBreaker() *gobreaker.CircuitBreaker {
    settings := gobreaker.Settings{
        Name:        "seckill",
        MaxRequests: 3,                     // 半开状态最大请求数
        Interval:    10 * time.Second,      // 统计周期
        Timeout:     30 * time.Second,      // 熔断后多久尝试恢复
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            // 失败率 > 50% 或 连续失败 > 5 次,触发熔断
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 10 && (failureRatio >= 0.5 || counts.ConsecutiveFailures > 5)
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            log.Infof("Circuit breaker %s state changed from %s to %s", name, from, to)
        },
    }

    return gobreaker.NewCircuitBreaker(settings)
}

// HTTP 中间件
func CircuitBreakerMiddleware(cb *gobreaker.CircuitBreaker) gin.HandlerFunc {
    return func(c *gin.Context) {
        _, err := cb.Execute(func() (interface{}, error) {
            c.Next()

            // 如果响应状态码 >= 500,视为失败
            if c.Writer.Status() >= 500 {
                return nil, errors.New("service error")
            }
            return nil, nil
        })

        if err != nil {
            // 熔断器打开,返回降级响应
            c.JSON(503, gin.H{
                "code":    50300,
                "message": "服务暂时不可用,请稍后重试",
            })
            c.Abort()
        }
    }
}

3. 分布式锁防止超卖

package lock

import (
    "context"
    "github.com/go-redis/redis/v8"
    "time"
)

type RedisLock struct {
    rdb *redis.Client
}

// Lock 获取分布式锁
func (l *RedisLock) Lock(ctx context.Context, key string, ttl time.Duration) (bool, error) {
    // SET key value NX EX ttl
    result, err := l.rdb.SetNX(ctx, key, "1", ttl).Result()
    return result, err
}

// Unlock 释放锁
func (l *RedisLock) Unlock(ctx context.Context, key string) error {
    return l.rdb.Del(ctx, key).Err()
}

// 使用示例:库存对账时加锁
func (s *SeckillService) ReconcileStock(ctx context.Context, seckillID int64) error {
    lockKey := fmt.Sprintf("lock:reconcile:%d", seckillID)

    // 尝试获取锁
    locked, err := s.lock.Lock(ctx, lockKey, 30*time.Second)
    if err != nil {
        return err
    }
    if !locked {
        return errors.New("对账任务正在进行中")
    }
    defer s.lock.Unlock(ctx, lockKey)

    // 执行对账逻辑
    // ...

    return nil
}

4. 多级缓存

【缓存层级】
L1: 本地缓存 (Caffeine/Go-Cache)
    - 商品详情(不变数据)
    - TTL: 5 分钟
    - 命中率: 90%

L2: Redis 集群
    - 库存、用户购买记录
    - TTL: 秒杀结束 + 1 小时
    - 命中率: 95%

L3: MySQL 主从
    - 持久化存储
    - 读写分离
package cache

import (
    "github.com/patrickmn/go-cache"
    "time"
)

type MultiLevelCache struct {
    local *cache.Cache
    redis *redis.Client
    db    *sql.DB
}

func NewMultiLevelCache(rdb *redis.Client, db *sql.DB) *MultiLevelCache {
    return &MultiLevelCache{
        local: cache.New(5*time.Minute, 10*time.Minute),
        redis: rdb,
        db:    db,
    }
}

// GetProduct 获取商品信息(三级缓存)
func (m *MultiLevelCache) GetProduct(ctx context.Context, seckillID int64) (*Product, error) {
    cacheKey := fmt.Sprintf("product:%d", seckillID)

    // L1: 本地缓存
    if val, found := m.local.Get(cacheKey); found {
        return val.(*Product), nil
    }

    // L2: Redis
    var product Product
    val, err := m.redis.Get(ctx, cacheKey).Result()
    if err == nil {
        json.Unmarshal([]byte(val), &product)
        m.local.Set(cacheKey, &product, cache.DefaultExpiration)
        return &product, nil
    }

    // L3: MySQL
    err = m.db.QueryRow("SELECT * FROM seckill_product WHERE id = ?", seckillID).Scan(&product)
    if err != nil {
        return nil, err
    }

    // 回写缓存
    data, _ := json.Marshal(product)
    m.redis.Set(ctx, cacheKey, data, 1*time.Hour)
    m.local.Set(cacheKey, &product, cache.DefaultExpiration)

    return &product, nil
}

5. 定时任务:超时订单取消

package job

import (
    "time"
)

type OrderTimeoutJob struct {
    db    *sql.DB
    redis *redis.Client
}

// Run 定时扫描超时订单
func (j *OrderTimeoutJob) Run() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        j.cancelTimeoutOrders()
    }
}

func (j *OrderTimeoutJob) cancelTimeoutOrders() {
    // 查询超时订单(支付超时时间 < 当前时间,且状态为待支付)
    query := `
        SELECT id, order_id, user_id, seckill_id, quantity
        FROM seckill_order
        WHERE status = 0 AND pay_expire_time < NOW()
        LIMIT 100
    `

    rows, err := j.db.Query(query)
    if err != nil {
        log.Error("查询超时订单失败", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        var id, userID, seckillID, quantity int64
        var orderID string
        rows.Scan(&id, &orderID, &userID, &seckillID, &quantity)

        // 取消订单
        j.cancelOrder(id, orderID, userID, seckillID, quantity)
    }
}

func (j *OrderTimeoutJob) cancelOrder(id int64, orderID string, userID, seckillID, quantity int64) {
    ctx := context.Background()

    // 开启事务
    tx, err := j.db.Begin()
    if err != nil {
        return
    }
    defer tx.Rollback()

    // 1. 更新订单状态为超时取消
    updateSQL := "UPDATE seckill_order SET status = 3 WHERE id = ? AND status = 0"
    result, err := tx.Exec(updateSQL, id)
    if err != nil {
        return
    }

    affected, _ := result.RowsAffected()
    if affected == 0 {
        return  // 订单已被处理
    }

    // 2. 回滚库存
    rollbackSQL := "UPDATE seckill_product SET available_stock = available_stock + ? WHERE id = ?"
    tx.Exec(rollbackSQL, quantity, seckillID)

    // 3. 删除用户购买记录
    deleteSQL := "DELETE FROM user_purchase WHERE user_id = ? AND seckill_id = ?"
    tx.Exec(deleteSQL, userID, seckillID)

    // 4. 记录库存流水
    insertLogSQL := `
        INSERT INTO stock_log (seckill_id, order_id, user_id, quantity, stock_before, stock_after, operation_type)
        VALUES (?, ?, ?, ?, 0, 0, 2)
    `
    tx.Exec(insertLogSQL, seckillID, orderID, userID, quantity)

    // 提交事务
    if err := tx.Commit(); err != nil {
        return
    }

    // 5. 回滚 Redis 库存
    stockKey := fmt.Sprintf("seckill:stock:%d", seckillID)
    userKey := fmt.Sprintf("seckill:users:%d", seckillID)
    j.redis.IncrBy(ctx, stockKey, quantity)
    j.redis.SRem(ctx, userKey, userID)

    log.Infof("订单 %s 超时取消,库存已回滚", orderID)
}

V3 架构优势

高可用保障:
┌────────────────────────────────────────────┐
│ 1. 多地域部署                               │
│    - 4 个地域(华北、华东、华南、西南)     │
│    - 就近接入,降低延迟                     │
│                                             │
│ 2. 服务多活                                 │
│    - 应用无状态,可水平扩展                 │
│    - 故障自动切换                           │
│                                             │
│ 3. 数据高可用                               │
│    - Redis 哨兵模式(主从自动切换)         │
│    - MySQL 主从 + 半同步复制                │
│    - Kafka 3 副本                           │
│                                             │
│ 4. 降级策略                                 │
│    - 熔断器保护下游                         │
│    - 限流保护系统                           │
│    - 缓存降级(返回静态数据)               │
│                                             │
│ 5. 监控告警                                 │
│    - 全链路监控                             │
│    - 实时告警                               │
│    - 自动扩缩容                             │
└────────────────────────────────────────────┘

核心算法与实现

1. 库存扣减算法

方案对比

方案实现方式优点缺点适用场景
悲观锁SELECT ... FOR UPDATE实现简单,数据强一致性能差,锁竞争激烈低并发场景
乐观锁UPDATE ... WHERE version = ?无锁,性能好高并发下大量失败重试中并发场景
Redis 原子操作DECR + Lua 脚本性能极高,原子性需要 Redis-DB 数据同步高并发秒杀

最佳实践:Redis + Lua

-- stock_decr.lua
-- KEYS[1]: 库存 key
-- KEYS[2]: 用户购买记录 key
-- ARGV[1]: 扣减数量
-- ARGV[2]: 用户 ID

local stock_key = KEYS[1]
local user_key = KEYS[2]
local quantity = tonumber(ARGV[1])
local user_id = ARGV[2]

-- 检查用户是否已购买
if redis.call('SISMEMBER', user_key, user_id) == 1 then
    return -2  -- 重复购买
end

-- 获取当前库存
local stock = tonumber(redis.call('GET', stock_key) or 0)

-- 检查库存是否充足
if stock < quantity then
    return -1  -- 库存不足
end

-- 扣减库存
redis.call('DECRBY', stock_key, quantity)

-- 记录用户购买
redis.call('SADD', user_key, user_id)

-- 返回剩余库存
return stock - quantity

2. 防重复购买

Bloom Filter(布隆过滤器)

package filter

import (
    "github.com/bits-and-blooms/bloom/v3"
)

type PurchaseFilter struct {
    filter *bloom.BloomFilter
}

func NewPurchaseFilter(expectedItems uint, falsePositiveRate float64) *PurchaseFilter {
    return &PurchaseFilter{
        filter: bloom.NewWithEstimates(expectedItems, falsePositiveRate),
    }
}

// MayExist 检查用户是否可能已购买(快速过滤)
func (p *PurchaseFilter) MayExist(userID, seckillID int64) bool {
    key := fmt.Sprintf("%d:%d", userID, seckillID)
    return p.filter.TestString(key)
}

// Add 添加购买记录
func (p *PurchaseFilter) Add(userID, seckillID int64) {
    key := fmt.Sprintf("%d:%d", userID, seckillID)
    p.filter.AddString(key)
}

// 使用示例
func (s *SeckillService) CreateOrder(userID, seckillID int64) (string, error) {
    // 第一步:Bloom Filter 快速判断
    if s.filter.MayExist(userID, seckillID) {
        // 可能已购买,进一步检查 Redis
        userKey := fmt.Sprintf("seckill:users:%d", seckillID)
        exists, _ := s.rdb.SIsMember(ctx, userKey, userID).Result()
        if exists {
            return "", errors.New("您已购买过该商品")
        }
    }

    // 继续秒杀流程...
}

3. 分布式 ID 生成(订单号)

package idgen

import (
    "fmt"
    "sync"
    "time"
)

// Snowflake ID 生成器
type SnowflakeIDGenerator struct {
    mu           sync.Mutex
    timestamp    int64
    machineID    int64  // 机器 ID (10 bits)
    sequence     int64  // 序列号 (12 bits)
}

const (
    epoch           = int64(1609459200000)  // 2021-01-01 00:00:00
    machineBits     = uint(10)
    sequenceBits    = uint(12)
    machineMax      = int64(-1) ^ (int64(-1) << machineBits)
    sequenceMask    = int64(-1) ^ (int64(-1) << sequenceBits)
    machineShift    = sequenceBits
    timestampShift  = machineBits + sequenceBits
)

func NewSnowflakeIDGenerator(machineID int64) *SnowflakeIDGenerator {
    if machineID < 0 || machineID > machineMax {
        panic("invalid machine ID")
    }

    return &SnowflakeIDGenerator{
        timestamp: 0,
        machineID: machineID,
        sequence:  0,
    }
}

func (s *SnowflakeIDGenerator) NextID() (int64, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    now := time.Now().UnixNano() / 1e6

    if now < s.timestamp {
        return 0, fmt.Errorf("clock moved backwards")
    }

    if now == s.timestamp {
        // 同一毫秒内,序列号自增
        s.sequence = (s.sequence + 1) & sequenceMask
        if s.sequence == 0 {
            // 序列号溢出,等待下一毫秒
            for now <= s.timestamp {
                now = time.Now().UnixNano() / 1e6
            }
        }
    } else {
        // 新的毫秒,序列号重置
        s.sequence = 0
    }

    s.timestamp = now

    id := ((now - epoch) << timestampShift) |
          (s.machineID << machineShift) |
          s.sequence

    return id, nil
}

// 生成订单号
func GenerateOrderID(machineID int64) string {
    gen := NewSnowflakeIDGenerator(machineID)
    id, _ := gen.NextID()
    return fmt.Sprintf("SK%d", id)
}

4. 限流算法

令牌桶算法

package ratelimit

import (
    "sync"
    "time"
)

type TokenBucket struct {
    capacity   int64        // 桶容量
    tokens     int64        // 当前令牌数
    rate       int64        // 令牌生成速率(个/秒)
    lastUpdate time.Time    // 上次更新时间
    mu         sync.Mutex
}

func NewTokenBucket(capacity, rate int64) *TokenBucket {
    return &TokenBucket{
        capacity:   capacity,
        tokens:     capacity,
        rate:       rate,
        lastUpdate: time.Now(),
    }
}

// TryAcquire 尝试获取令牌
func (tb *TokenBucket) TryAcquire(count int64) bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    // 计算应该新增的令牌数
    now := time.Now()
    elapsed := now.Sub(tb.lastUpdate).Seconds()
    newTokens := int64(elapsed * float64(tb.rate))

    // 更新令牌数(不超过容量)
    tb.tokens += newTokens
    if tb.tokens > tb.capacity {
        tb.tokens = tb.capacity
    }
    tb.lastUpdate = now

    // 检查是否有足够的令牌
    if tb.tokens >= count {
        tb.tokens -= count
        return true
    }

    return false
}

// 使用示例
var globalBucket = NewTokenBucket(100000, 10000)  // 容量 10 万,速率 1 万/秒

func HandleRequest() {
    if !globalBucket.TryAcquire(1) {
        // 限流
        return errors.New("系统繁忙,请稍后再试")
    }

    // 处理请求...
}

优化方案

1. 页面静态化

【优化目标】
减少动态请求,降低服务器压力

【实现方案】
1. 商品详情页面静态化
   - 提前生成 HTML 文件
   - 部署到 CDN
   - 只有库存和倒计时动态更新(AJAX)

2. 前端优化
   - 按钮防抖(1 秒内只能点击一次)
   - 前端倒计时(减少时间接口调用)
   - 资源合并压缩(减少 HTTP 请求)
<!-- 静态化示例 -->
<!DOCTYPE html>
<html>
<head>
    <title>iPhone 15 Pro Max 秒杀</title>
    <script src="https://cdn.example.com/jquery.min.js"></script>
</head>
<body>
    <div class="product">
        <img src="https://cdn.example.com/iphone15.jpg" />
        <h1>iPhone 15 Pro Max 256G</h1>
        <div class="price">
            <span class="original">¥9999</span>
            <span class="seckill">¥7999</span>
        </div>

        <!-- 动态库存 -->
        <div class="stock">剩余库存:<span id="stock">加载中...</span></div>

        <!-- 倒计时 -->
        <div class="countdown">距离开始:<span id="countdown"></span></div>

        <!-- 秒杀按钮 -->
        <button id="seckill-btn" disabled>立即秒杀</button>
    </div>

    <script>
    // 前端倒计时
    let serverTime = 1699804800000;  // 服务器时间
    let startTime = 1699804800000;   // 秒杀开始时间

    setInterval(() => {
        serverTime += 1000;
        let diff = startTime - serverTime;

        if (diff <= 0) {
            $("#seckill-btn").attr("disabled", false);
            $("#countdown").text("秒杀进行中");
        } else {
            let hours = Math.floor(diff / 3600000);
            let minutes = Math.floor((diff % 3600000) / 60000);
            let seconds = Math.floor((diff % 60000) / 1000);
            $("#countdown").text(`${hours}:${minutes}:${seconds}`);
        }
    }, 1000);

    // 动态获取库存(轮询)
    setInterval(() => {
        $.get("/api/v1/seckill/stock/1", function(data) {
            $("#stock").text(data.stock);
        });
    }, 1000);

    // 秒杀按钮防抖
    let clicking = false;
    $("#seckill-btn").click(function() {
        if (clicking) return;
        clicking = true;

        // 先获取令牌
        $.post("/api/v1/seckill/token", {product_id: 1}, function(tokenResp) {
            // 再下单
            $.post("/api/v1/seckill/orders", {
                product_id: 1,
                quantity: 1,
                token: tokenResp.data.token
            }, function(resp) {
                if (resp.code === 0) {
                    alert("秒杀成功!订单号:" + resp.data.order_id);
                    window.location.href = "/pay/" + resp.data.order_id;
                } else {
                    alert(resp.message);
                }
                clicking = false;
            }).fail(function() {
                clicking = false;
            });
        });
    });
    </script>
</body>
</html>

2. 缓存预热

package job

// 秒杀开始前 1 小时预热缓存
type CacheWarmupJob struct {
    db    *sql.DB
    redis *redis.Client
}

func (j *CacheWarmupJob) Warmup() {
    // 查询即将开始的秒杀活动
    query := `
        SELECT id, product_id, title, original_price, seckill_price, stock, available_stock, start_time, end_time
        FROM seckill_product
        WHERE start_time > NOW() AND start_time < DATE_ADD(NOW(), INTERVAL 1 HOUR)
    `

    rows, err := j.db.Query(query)
    if err != nil {
        return
    }
    defer rows.Close()

    ctx := context.Background()

    for rows.Next() {
        var product SeckillProduct
        rows.Scan(&product.ID, &product.ProductID, &product.Title, ...)

        // 1. 缓存商品详情
        productKey := fmt.Sprintf("seckill:product:%d", product.ID)
        data, _ := json.Marshal(product)
        j.redis.Set(ctx, productKey, data, 2*time.Hour)

        // 2. 缓存库存
        stockKey := fmt.Sprintf("seckill:stock:%d", product.ID)
        j.redis.Set(ctx, stockKey, product.AvailableStock, 2*time.Hour)

        // 3. 初始化用户购买记录集合
        userKey := fmt.Sprintf("seckill:users:%d", product.ID)
        j.redis.Del(ctx, userKey)  // 清空旧数据

        log.Infof("预热秒杀商品 %d 成功", product.ID)
    }
}

3. 数据库优化

读写分离

package db

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

type DBCluster struct {
    master *sql.DB
    slaves []*sql.DB
    slaveIndex int
}

func NewDBCluster(masterDSN string, slaveDSNs []string) (*DBCluster, error) {
    master, err := sql.Open("mysql", masterDSN)
    if err != nil {
        return nil, err
    }

    slaves := make([]*sql.DB, len(slaveDSNs))
    for i, dsn := range slaveDSNs {
        slave, err := sql.Open("mysql", dsn)
        if err != nil {
            return nil, err
        }
        slaves[i] = slave
    }

    return &DBCluster{
        master: master,
        slaves: slaves,
    }, nil
}

// GetMaster 获取主库(写)
func (c *DBCluster) GetMaster() *sql.DB {
    return c.master
}

// GetSlave 获取从库(读,轮询负载均衡)
func (c *DBCluster) GetSlave() *sql.DB {
    if len(c.slaves) == 0 {
        return c.master
    }

    slave := c.slaves[c.slaveIndex%len(c.slaves)]
    c.slaveIndex++
    return slave
}

分库分表

package sharding

import (
    "hash/crc32"
)

const (
    DBCount    = 8  // 8 个库
    TableCount = 8  // 每库 8 张表
)

// ShardingStrategy 分片策略
type ShardingStrategy struct{}

// GetShardDB 根据 user_id 计算库索引
func (s *ShardingStrategy) GetShardDB(userID int64) int {
    return int(userID % DBCount)
}

// GetShardTable 根据 user_id 计算表索引
func (s *ShardingStrategy) GetShardTable(userID int64) int {
    return int((userID / DBCount) % TableCount)
}

// GetTableName 获取实际表名
func (s *ShardingStrategy) GetTableName(userID int64) string {
    dbIndex := s.GetShardDB(userID)
    tableIndex := s.GetShardTable(userID)
    return fmt.Sprintf("db%d.seckill_order_%d", dbIndex, tableIndex)
}

// 使用示例
func (s *SeckillService) InsertOrder(order *Order) error {
    strategy := &ShardingStrategy{}
    tableName := strategy.GetTableName(order.UserID)

    sql := fmt.Sprintf(`
        INSERT INTO %s (order_id, user_id, product_id, ...)
        VALUES (?, ?, ?, ...)
    `, tableName)

    _, err := s.db.Exec(sql, order.OrderID, order.UserID, ...)
    return err
}

4. 消息队列优化

Kafka 生产者配置

package kafka

import (
    "github.com/Shopify/sarama"
)

func NewProducer(brokers []string) (sarama.SyncProducer, error) {
    config := sarama.NewConfig()

    // 性能优化配置
    config.Producer.RequiredAcks = sarama.WaitForLocal   // 只等待 leader 确认
    config.Producer.Compression = sarama.CompressionSnappy  // 压缩
    config.Producer.Flush.Messages = 100                 // 批量发送
    config.Producer.Flush.Frequency = 10 * time.Millisecond
    config.Producer.Return.Successes = true

    return sarama.NewSyncProducer(brokers, config)
}

消费者并发控制

package consumer

type OrderConsumerGroup struct {
    workers int
    jobs    chan *OrderMessage
    wg      sync.WaitGroup
}

func NewOrderConsumerGroup(workers int) *OrderConsumerGroup {
    return &OrderConsumerGroup{
        workers: workers,
        jobs:    make(chan *OrderMessage, 1000),
    }
}

func (c *OrderConsumerGroup) Start() {
    // 启动多个 worker 并发消费
    for i := 0; i < c.workers; i++ {
        c.wg.Add(1)
        go c.worker(i)
    }
}

func (c *OrderConsumerGroup) worker(id int) {
    defer c.wg.Done()

    for msg := range c.jobs {
        if err := c.processOrder(msg); err != nil {
            log.Errorf("Worker %d 处理订单失败: %v", id, err)
            // 重试或发送到死信队列
        }
    }
}

func (c *OrderConsumerGroup) Submit(msg *OrderMessage) {
    c.jobs <- msg
}

监控告警

核心监控指标

1. 业务指标

【Prometheus 指标定义】

# 秒杀请求总数
seckill_requests_total{api="/seckill/orders", status="success|failure"}

# 秒杀请求响应时间
seckill_request_duration_seconds{api="/seckill/orders", quantile="0.5|0.9|0.99"}

# 库存剩余量
seckill_stock_remaining{seckill_id="1"}

# 订单创建速率
seckill_orders_created_total

# 支付成功率
seckill_payment_success_rate
package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    // 请求总数
    RequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "seckill_requests_total",
            Help: "Total number of seckill requests",
        },
        []string{"api", "status"},
    )

    // 响应时间
    RequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "seckill_request_duration_seconds",
            Help:    "Seckill request duration",
            Buckets: prometheus.DefBuckets,
        },
        []string{"api"},
    )

    // 库存剩余
    StockRemaining = promauto.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "seckill_stock_remaining",
            Help: "Remaining stock of seckill products",
        },
        []string{"seckill_id"},
    )
)

// 使用示例
func (s *SeckillService) CreateOrder(...) {
    timer := prometheus.NewTimer(RequestDuration.WithLabelValues("/seckill/orders"))
    defer timer.ObserveDuration()

    // 业务逻辑...

    if err != nil {
        RequestsTotal.WithLabelValues("/seckill/orders", "failure").Inc()
        return err
    }

    RequestsTotal.WithLabelValues("/seckill/orders", "success").Inc()
    return nil
}

2. 系统指标

【关键指标】
- CPU 使用率 > 80%
- 内存使用率 > 85%
- 磁盘 IO 等待 > 50%
- 网络带宽使用率 > 80%
- DB 连接数 > 800
- Redis 连接数 > 8000
- Kafka 消费延迟 > 10 秒

3. Grafana 看板

【看板布局】
┌────────────────────────────────────────────────┐
│  秒杀系统实时监控大屏                           │
├────────────────────────────────────────────────┤
│  [实时 QPS]  [成功率]  [P99 延迟]  [库存剩余]  │
│   50,000     99.5%     120ms        500        │
├────────────────────────────────────────────────┤
│  [QPS 曲线图(1 小时)]                         │
│  ▁▂▃▅▇█████▇▅▃▂▁                               │
├────────────────────────────────────────────────┤
│  [各接口响应时间分布]                           │
│  /products: 20ms                                │
│  /orders:   100ms                               │
│  /pay:      200ms                               │
├────────────────────────────────────────────────┤
│  [错误率告警]                                   │
│  ⚠️  库存不足: 1000 次/分钟                    │
│  ⚠️  限流: 500 次/分钟                         │
└────────────────────────────────────────────────┘

告警规则

# Prometheus 告警规则
groups:
  - name: seckill_alerts
    rules:
      # QPS 过高
      - alert: HighQPS
        expr: rate(seckill_requests_total[1m]) > 100000
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "秒杀 QPS 过高"
          description: "当前 QPS {{ $value }} 超过阈值 100000"

      # 错误率过高
      - alert: HighErrorRate
        expr: rate(seckill_requests_total{status="failure"}[1m]) / rate(seckill_requests_total[1m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "秒杀错误率过高"
          description: "错误率 {{ $value }} 超过 5%"

      # 响应时间过长
      - alert: HighLatency
        expr: histogram_quantile(0.99, rate(seckill_request_duration_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "秒杀 P99 延迟过高"
          description: "P99 延迟 {{ $value }}s 超过 1 秒"

      # 库存异常
      - alert: StockAnomalous
        expr: seckill_stock_remaining < 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "秒杀库存异常"
          description: "商品 {{ $labels.seckill_id }} 库存为负数"

      # DB 连接数过高
      - alert: HighDBConnections
        expr: mysql_global_status_threads_connected > 800
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "MySQL 连接数过高"
          description: "连接数 {{ $value }} 超过 800"

面试问答

如何防止超卖?

【多重保障】
1. Redis 原子操作
   - Lua 脚本保证 DECR 原子性
   - 单线程模型,天然无并发问题

2. 数据库悲观锁(兜底)
   - SELECT ... FOR UPDATE
   - 防止 Redis 故障导致超卖

3. 库存流水对账
   - 每小时对账 Redis 和 DB
   - 发现不一致立即告警

4. 分布式锁(可选)
   - 关键操作加 Redis 分布式锁
   - 防止分布式环境并发问题

【关键点】
- Redis 为主(性能),DB 为辅(兜底)
- 异步补偿机制
- 实时监控库存异常

如何处理高并发流量?

【分层限流】
1. CDN 层
   - 静态资源缓存
   - 页面缓存

2. 接入层(Nginx)
   - IP 限流: 1000 req/s
   - 连接数限制

3. 网关层
   - 全局限流: 10 万 QPS
   - 用户限流: 10 req/s

4. 服务层
   - 令牌桶算法
   - 熔断降级

【削峰填谷】
- Kafka 消息队列
- 异步处理订单
- 平滑流量曲线

Redis 和 MySQL 数据不一致怎么办?

【一致性策略】
1. 正常流程
   - 先扣 Redis(快速响应)
   - 再异步更新 DB(Kafka 消费)

2. 不一致场景
   场景 1: Redis 扣减成功,DB 更新失败
     → Kafka 重试 3 次
     → 失败后回滚 Redis + 告警

   场景 2: Redis 扣减成功,Kafka 丢消息
     → Kafka ACK 机制保证
     → 多副本持久化

   场景 3: Redis 故障重启
     → 启动时从 DB 加载库存
     → 缓存预热

3. 对账机制
   - 定时任务每小时对账
   - Redis 库存 = DB 库存 - 待支付订单数
   - 不一致则告警并修复

【最终一致性】
- 秒杀场景可接受短暂不一致
- 但最终必须一致(对账修复)

如何防止黄牛刷单?

【多维度防刷】
1. 令牌机制
   - 秒杀前获取一次性令牌
   - 令牌包含签名,防伪造
   - 有效期 10 秒

2. 用户行为分析
   - IP 限流(防机房 IP)
   - 设备指纹(防模拟器)
   - 用户画像(新用户、异常行为)

3. 验证码
   - 高峰期动态开启
   - 滑动验证(防自动化)

4. 风控系统
   - 实时计算风险分数
   - 高风险用户降级处理

5. 实名认证
   - 手机号验证
   - 一人限购一件

如何保证订单不丢失?

【可靠性保障】
1. Kafka 高可用
   - 3 副本
   - ISR 机制(至少 2 个副本确认)
   - Producer ACK = all

2. 消费者幂等
   - 订单号唯一索引
   - 重复消费自动去重

3. 死信队列
   - 消费失败 3 次 → DLQ
   - 人工介入处理

4. 补偿机制
   - Redis 记录订单创建事件
   - 定时扫描未入库订单
   - 主动拉取补偿

5. 监控告警
   - Kafka Lag 监控
   - 消费异常告警

秒杀系统的性能瓶颈在哪里?

【瓶颈分析】
1. 数据库写入
   - 问题: 单库 TPS 1000 左右
   - 方案: 异步入库 + 分库分表

2. 网络带宽
   - 问题: 大量请求占用带宽
   - 方案: CDN + 页面静态化

3. Redis 单线程
   - 问题: 单实例 10 万 QPS 上限
   - 方案: 分片 + 多实例

4. 锁竞争
   - 问题: 悲观锁导致阻塞
   - 方案: Redis 原子操作

【优化思路】
- 能异步的绝不同步
- 能缓存的绝不查库
- 能限流的绝不硬抗

秒杀结束后如何处理未支付订单?

【订单生命周期】
1. 创建订单
   - 设置支付超时时间(15 分钟)
   - 状态: pending

2. 定时任务扫描
   - 每 10 秒扫描一次
   - 查询 pay_expire_time < NOW() 的订单

3. 取消订单
   - 更新状态为 timeout
   - 回滚 DB 库存
   - 回滚 Redis 库存
   - 删除用户购买记录
   - 记录库存流水

4. 通知用户
   - 短信/Push 通知
   - "订单已取消,库存已释放"

【延迟队列方案(更优)】
- 下单时发送延迟消息到 Kafka
- 15 分钟后消费
- 检查订单状态,未支付则取消

如何测试秒杀系统?

【压测方案】
1. 工具选择
   - JMeter / Gatling
   - Locust (Python)
   - 自研压测平台

2. 压测场景
   场景 1: 正常秒杀
     - 10 万用户并发
     - 成功率 > 99%
     - P99 < 500ms

   场景 2: 超卖验证
     - 库存 100 件
     - 10 万用户抢购
     - 最终订单数 = 100(绝不超卖)

   场景 3: 限流验证
     - QPS 超过阈值
     - 返回 429 状态码

   场景 4: 故障演练
     - Redis 主节点宕机
     - DB 从库宕机
     - Kafka 节点宕机
     - 观察系统自愈能力

3. 性能基线
   - 记录每次压测结果
   - 建立性能基线
   - 性能回归测试

【全链路压测】
- 生产环境流量复制
- 影子表 + 影子库
- 真实模拟大促场景

秒杀系统的 SLA 如何设计?

【SLA 指标】
1. 可用性
   - 目标: 99.99%(全年停机 < 53 分钟)
   - 保障: 多活架构 + 故障自动切换

2. 性能
   - QPS: 支持 10 万
   - 响应时间: P99 < 500ms
   - 保障: 限流 + 降级

3. 准确性
   - 超卖率: 0
   - 订单丢失率: < 0.01%
   - 保障: 强一致性 + 补偿机制

4. 容量
   - 支持 100 万用户同时在线
   - 单场秒杀最多 1 万库存
   - 保障: 弹性扩容

【降级策略】
- 非核心功能降级(评论、推荐)
- 只读降级(查询返回缓存)
- 限流降级(拒绝部分请求)

如果让你从 0 到 1 设计秒杀系统,步骤是什么?

【设计步骤】
1. 需求分析(5 分钟)
   - 规模: 100 万用户,10 万 QPS
   - 功能: 商品展示、秒杀下单、支付
   - 一致性: 库存强一致,订单最终一致

2. 容量估算(5 分钟)
   - QPS: 20 万
   - 带宽: 10 Gbps
   - 存储: 2 TB/年
   - 服务器: 300 台

3. API 设计(5 分钟)
   - GET /products
   - POST /orders
   - POST /token

4. 数据模型(5 分钟)
   - seckill_product
   - seckill_order
   - stock_log

5. 架构设计(20 分钟)
   - V1: 单体 + MySQL(快速验证)
   - V2: Redis + Kafka(支撑 10 万 QPS)
   - V3: 多活 + 全链路优化(生产级)

6. 核心难点(15 分钟)
   - 超卖防止: Redis Lua + DB 兜底
   - 高并发: 多级限流 + 削峰填谷
   - 数据一致性: 异步入库 + 对账

7. 优化方案(5 分钟)
   - 页面静态化
   - 缓存预热
   - 读写分离
   - 分库分表

【加分项】
- 画架构图
- 写核心代码
- 分析性能瓶颈
- 提出监控方案

Prev
01-短链系统设计
Next
03 - IM 即时通讯系统设计