02 - 秒杀系统设计
面试频率: 难度等级: 推荐时长: 45-60 分钟
目录
需求分析与澄清
业务场景
秒杀系统是电商平台的核心业务场景之一,特点是短时间内大量用户抢购有限库存商品。典型场景包括:
- 双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 分钟)
- 页面静态化
- 缓存预热
- 读写分离
- 分库分表
【加分项】
- 画架构图
- 写核心代码
- 分析性能瓶颈
- 提出监控方案