目录
正在加载目录…
专栏文章
专栏文章
性能工程专栏
1. 高并发系统设计:架构演进与关键技术选型 2. 稳定性与容灾设计:限流、熔断、降级与隔离 3. 全链路压测:容量验证、流量构造与瓶颈定位 4. 秒杀系统设计:库存、防超卖与高并发治理 5. 性能优化方法论:指标分析与分层调优策略 6. 性能测试体系:压测方法、指标与瓶颈定位 7. 容量规划与弹性:水位线、扩缩容与大促保障

秒杀系统设计:库存、防超卖与高并发治理

发布于 2026-08-19 15:02 · 最后编辑于 2026-08-19 15:03 · 字数 3,134 👁 28 次阅读

本文系统梳理秒杀系统的设计方法:从核心挑战出发,讲解分层架构(CDN→Nginx→网关→应用层→缓存层→数据库)、库存扣减方案(先扣缓存/数据库乐观锁/消息队列异步)、防超卖三层防护、限流策略、预热方案,以及监控与降级。包含完整架构图(Mermaid)和每层技术选型说明。

性能工程系列高并发系统设计 · 性能优化方法论 · 相关Redis 简介 · 消息队列核心原理

目录

章节说明
秒杀核心挑战瞬时高并发、超卖、恶意刷单
架构设计原则4 要 1 不要
分层架构设计完整架构图与每层职责
流量削峰方案验证码、消息队列、限流漏斗
库存扣减方案三种方案对比与极致优化
防超卖三层防护Redis SETNX / Lua / DB 唯一约束
限流策略用户维度/商品维度/全局限流
预热方案提前加载热数据与预生成令牌
隔离策略秒杀与普通流量隔离
降级与容灾熔断、降级、兜底方案
监控体系关键指标与告警

秒杀核心挑战

三大挑战

1. 瞬时高并发

秒杀的流量特征:毛刺极大,几秒内爬升到峰值,然后马上掉下来。

正常流量:100 QPS
秒杀瞬间:100,000 QPS(100倍)
持续时间:< 10 秒(库存卖完即结束)

这种流量特征导致:

  • 按峰值配置机器 → 资源浪费(99% 时间空转)
  • 不配置峰值容量 → 秒杀时系统崩溃

2. 热点数据问题

所有用户抢购同一商品,该商品的库存 Key 成为极热点:

  • Redis 单个 Key 写入上限约 5-10 万 TPS
  • MySQL 单行记录并发更新,InnoDB 行锁竞争激烈

3. 刷子流量(黄牛)

通过程序直接调用 HTTP 接口,绕过前端限制:

  • 挤占正常用户的抢购通道
  • 给系统带来大量无效请求
  • 破坏公平的抢购环境

架构设计原则

4 要 1 不要

原则说明示例
数据要尽量少减少传输数据量,降低 CPU 序列化开销秒杀页面去掉装修效果,只保留核心信息
请求数要尽量少合并 CSS/JS,减少 DNS 解析,减少 TCP 握手多个 JS 文件合并为一个 URL
路径要尽量短减少中间节点,每增加一个节点增加一个故障点RPC 调用合并,减少跨服务依赖
依赖要尽量少减少强依赖,弱依赖可在紧急时降级优惠券是弱依赖,可降级;库存是强依赖
不要有单点服务无状态化,状态外置到存储配置中心动态推送,避免机器绑定

架构是平衡的艺术:以上原则是方向,不是绝对。如把首屏 CSS 内联可减少请求数,但增大了页面体积,需要权衡。

分层架构设计

秒杀系统整体架构

完整架构图

flowchart TD
    User["用户"] --> DNS["DNS 层<br/>网络防攻击<br/>(DDoS 防护、IP 封禁)"]
    DNS --> CDN["CDN 层<br/>静态资源缓存<br/>商品图片/页面骨架/JS/CSS"]
    CDN -->|"动态请求"| Nginx["Nginx 层<br/>反向代理 + 负载均衡<br/>静态资源服务<br/>Nginx+Lua 业务校验(防刷)<br/>连接数限流"]
    Nginx --> Gateway["网关层<br/>认证鉴权<br/>全局限流(令牌桶)<br/>黑名单过滤<br/>风控拦截"]
    Gateway --> Web["Web 服务层<br/>业务聚合<br/>结算页渲染<br/>本地缓存(LocalCache)<br/>用户维度限流"]
    Web --> RPC["RPC 服务层<br/>秒杀核心逻辑<br/>库存扣减<br/>订单生成"]
    RPC --> Redis["Redis 缓存层<br/>库存预扣<br/>令牌桶<br/>用户请求去重"]
    RPC --> MQ["消息队列<br/>Kafka/RocketMQ<br/>异步下单削峰"]
    MQ --> OrderSvc["订单服务<br/>消费 MQ 消息<br/>生成订单记录"]
    OrderSvc --> MySQL["MySQL 数据库<br/>库存最终扣减<br/>订单持久化"]
    style DNS fill:#fcc,stroke:#c00
    style CDN fill:#cfc,stroke:#060
    style Nginx fill:#cfc,stroke:#060
    style Gateway fill:#cfc,stroke:#060
    style Web fill:#cfc,stroke:#060
    style Redis fill:#cfc,stroke:#060

各层职责与技术选型

层级职责技术选型拦截目标
DNS 层网络层防攻击云厂商 DDoS 防护网络攻击流量
CDN 层静态资源就近分发阿里云 CDN / CloudFlare静态资源请求(图片/JS/CSS)
Nginx 层反向代理、防刷、限流Nginx + Lua(OpenResty)IP 级别高频请求
网关层认证、全局限流、风控自研网关 / Kong未登录请求、黑名单用户
Web 服务层业务聚合、本地缓存Spring Boot无效业务请求(活动未开始等)
RPC 服务层秒杀核心逻辑Dubbo / gRPC超出库存的请求
缓存层库存预扣、令牌Redis Cluster库存不足的请求
消息队列异步削峰Kafka / RocketMQ平滑瞬时写峰值
数据库最终一致性保证MySQL兜底防超卖

核心原则:校验前置、分层过滤。流量经过每一层后都被大幅削减,到达数据库的请求只剩极少量。

流量削峰方案

验证码与问答题(无损削峰)

目的

  1. 平滑毛刺流量:将 1s 内的瞬时流量分散到 30s ~ 1min
  2. 防机器刷单:增加自动化程序的成本

实现原理

生成验证码 → 计算结果存入 Redis(Key: CAPTCHA_{user}_{skuId},TTL 60s)
用户提交答案 → 与 Redis 中结果比对 → 通过则允许下单

效果:不同用户手速不同,1 万个并发请求可以平滑到 1 分钟内处理,系统只需支撑 1/60 的峰值压力。

消息队列异步化(无损削峰)

sequenceDiagram
    participant User as 用户
    participant Web as Web 服务
    participant Redis as Redis
    participant MQ as 消息队列
    participant Consumer as 消费者
    participant DB as MySQL

    User->>Web: 点击秒杀
    Web->>Redis: 预扣库存(DECR)
    Redis-->>Web: 库存 > 0,扣减成功
    Web->>MQ: 发送下单消息
    Web-->>User: 返回"排队中"
    Consumer->>MQ: 消费下单消息
    Consumer->>DB: 创建订单,扣减 DB 库存
    Consumer-->>User: 通知秒杀结果(推送/轮询)

容量规划

商品库存:1000 件
单次处理时间:500ms
部署 10 个消费者 → 总处理时间:50s(用户等待上限)
消息队列堆积量 = 秒杀请求数 - 已处理数(需监控)

分层过滤(有损削峰)

graph TD
    Total["总请求 100,000"] --> L1["Nginx 层过滤<br/>IP 限流、黑名单<br/>剩余:50,000"]
    L1 --> L2["网关层过滤<br/>认证、风控<br/>剩余:30,000"]
    L2 --> L3["应用层过滤<br/>活动状态、用户资格<br/>剩余:10,000"]
    L3 --> L4["缓存层过滤<br/>Redis 库存预扣<br/>剩余:1,000(实际库存)"]
    L4 --> L5["数据库<br/>最终扣减<br/>1,000 笔成功"]
    style Total fill:#fcc,stroke:#c00
    style L5 fill:#cfc,stroke:#060

库存扣减方案

三种方案对比

方案时机超卖风险恶意下单风险复杂度
下单减库存下单时立即扣减高(下单不付款)
付款减库存付款时扣减高(下单数 >> 库存)
预扣库存下单时预扣,超时释放

秒杀场景推荐下单减库存

理由:

  • 秒杀商品"抢到就是赚到",成功下单后不付款的概率很低
  • 逻辑简单,性能更好
  • 卖家对库存有严格限制,不允许超卖

防止库存为负数

-- 方案 1:事务 + 应用层判断
BEGIN;
SELECT inventory FROM item WHERE id = ? FOR UPDATE;
-- 应用层判断 inventory >= 购买数量
UPDATE item SET inventory = inventory - ? WHERE id = ?;
COMMIT;

-- 方案 2:无符号整数(减后 < 0 则 SQL 报错)
inventory BIGINT UNSIGNED NOT NULL

-- 方案 3:CASE WHEN(推荐,避免行锁超时)
UPDATE item SET inventory = CASE 
    WHEN inventory >= #{count} THEN inventory - #{count} 
    ELSE inventory 
END 
WHERE id = #{itemId}

极致优化:Redis 预扣 + DB 兜底

场景:简单库存逻辑(无复杂 SKU 联动)

Redis 预扣:
  DECR seckill:inventory:{itemId}
  返回值 >= 0 → 预扣成功,发 MQ 消息
  返回值 < 0 → 库存不足,INCR 回滚

DB 最终扣减:
  消费 MQ 消息 → UPDATE item SET inventory = inventory - 1 WHERE id = ? AND inventory > 0
  影响行数 = 0 → 扣减失败(兜底防超卖)

数据库层并发优化

问题:大量并发更新同一行,InnoDB 行锁竞争激烈,TPS 下降,RT 上升。

解决方案 1:应用层排队

  • 按商品 ID 维度设置队列,单机串行处理同一商品的扣减请求
  • 控制单个商品占用的 DB 连接数
  • 局限:只能控制单机并发,集群场景效果有限

解决方案 2:热点数据隔离

  • 将热点商品迁移到独立的热点库
  • 减少热点商品对其他商品的影响

解决方案 3:阿里 MySQL 补丁(InnoDB 层排队)

  • 在数据库层对单行记录做全局排队
  • COMMIT_ON_SUCCESS + ROLLBACK_ON_FAIL:事务结束后立即提交/回滚,减少网络等待(约 0.7ms)

防超卖三层防护

graph TD
    A["第一层:Redis Lua 脚本<br/>原子性检查并扣减库存"] --> B{"库存 > 0?"}
    B -->|否| C["拒绝请求,返回库存不足"]
    B -->|是| D["第二层:消息队列幂等消费<br/>消费者处理前检查订单是否已存在"]
    D --> E{"订单已存在?"}
    E -->|是| F["幂等处理,直接返回成功"]
    E -->|否| G["第三层:数据库唯一约束<br/>订单表 (user_id, item_id) 唯一索引<br/>+ inventory > 0 检查"]
    G --> H{"DB 扣减成功?"}
    H -->|否| I["回滚,通知用户失败"]
    H -->|是| J["秒杀成功"]
    style C fill:#fcc,stroke:#c00
    style J fill:#cfc,stroke:#060

Redis Lua 脚本(原子性)

-- 原子性检查并扣减库存
local key = KEYS[1]
local count = tonumber(ARGV[1])
local inventory = tonumber(redis.call('get', key))
if inventory == nil then
    return -1  -- key 不存在
end
if inventory < count then
    return 0   -- 库存不足
end
return redis.call('decrby', key, count)  -- 扣减成功,返回剩余库存

为什么用 Lua:Redis 执行 Lua 脚本是原子的,避免 GET + DECR 之间的竞态条件。

数据库唯一约束(兜底)

-- 订单表唯一约束,防止重复下单
CREATE UNIQUE INDEX uk_user_item ON seckill_order(user_id, item_id, activity_id);

-- 库存扣减时附加条件
UPDATE seckill_item 
SET inventory = inventory - 1 
WHERE item_id = ? AND inventory > 0;
-- 影响行数 = 0 表示库存不足,回滚事务

限流策略

多维度限流

维度算法实现说明
用户维度固定窗口计数Redis INCR + EXPIRE同一用户 N 秒内最多请求 M 次
商品维度令牌桶Redis + Lua控制单个商品的处理速率
全局维度漏桶 / 令牌桶Nginx limit_req控制整体 QPS 上限
IP 维度滑动窗口Nginx + Redis防止单 IP 刷接口

常用限流算法

令牌桶(Token Bucket)

  • 以固定速率往桶里放令牌(如 1000 个/秒)
  • 每个请求消耗一个令牌,桶空则拒绝
  • 允许突发流量(桶满时可以瞬间消耗)

漏桶(Leaky Bucket)

  • 请求进入队列,以固定速率处理
  • 队列满则丢弃新请求
  • 严格限制输出速率,不允许突发

滑动窗口

  • 统计最近 N 秒内的请求数
  • 比固定窗口更精确,无边界突刺问题

Nginx 层限流配置

# 按 IP 限流:每秒最多 100 个请求
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=100r/s;

# 按用户 ID 限流(从 Cookie 中提取)
limit_req_zone $cookie_user_id zone=user_limit:10m rate=10r/s;

location /seckill/buy {
    limit_req zone=ip_limit burst=20 nodelay;
    limit_req zone=user_limit burst=5;
    proxy_pass http://backend;
}

预热方案

活动开始前的预热步骤

T-24h:运营配置秒杀活动(商品、库存、时间)
T-1h:系统预加载活动数据到 Redis
  - 库存:SET seckill:inventory:{itemId} {count}
  - 活动信息:HMSET seckill:activity:{activityId} ...
  - 预生成令牌:LPUSH seckill:token:{activityId} {token1} {token2} ...
T-0:活动开始,直接从 Redis 读取数据

预生成令牌

思路:活动开始前,按库存数量预生成令牌,放入 Redis 队列。用户请求时从队列中弹出令牌,弹出成功才允许下单。

// 预热:生成 N 个令牌
void warmUp(long activityId, int inventory) {
    String key = "seckill:token:" + activityId;
    for (int i = 0; i < inventory; i++) {
        redis.rpush(key, UUID.randomUUID().toString());
    }
    redis.expire(key, 3600); // 1小时过期
}

// 秒杀:弹出令牌
String token = redis.lpop("seckill:token:" + activityId);
if (token == null) {
    return "库存不足";
}
// 凭 token 下单

优势

  • LPOP 是原子操作,天然防超卖
  • 令牌数量 = 库存数量,精确控制
  • 比 DECR 更灵活(可以做令牌绑定用户等逻辑)

本地缓存预热

// Web 服务层:活动开始前加载到本地缓存
@Scheduled(fixedDelay = 60000) // 每分钟刷新
void refreshLocalCache() {
    List<Activity> activities = activityService.getActiveActivities();
    for (Activity activity : activities) {
        localCache.put("activity:" + activity.getId(), activity);
    }
}

隔离策略

为什么要隔离

秒杀流量是普通流量的 100 倍以上,如果混在一起:

  • 秒杀流量会拖垮普通商品的购买
  • 秒杀系统的故障会影响整个电商平台

隔离层次

层次隔离方式说明
系统隔离独立部署秒杀系统独立的应用集群、独立的 DB、独立的缓存
域名隔离独立域名(seckill.xxx.com)CDN、Nginx 可按域名单独配置
数据隔离秒杀商品数据独立存储热点商品放独立 Redis 实例,不影响公共缓存
线程池隔离Hystrix/Sentinel 线程池隔离秒杀接口使用独立线程池,故障不影响其他接口

降级与容灾

降级策略

graph TD
    Normal["正常状态<br/>全功能"] -->|"Redis 故障"| Degrade1["降级 1:关闭 Redis 预扣<br/>直接走 DB(有超卖风险)"]
    Normal -->|"DB 故障"| Degrade2["降级 2:关闭下单入口<br/>展示'系统繁忙'"]
    Normal -->|"MQ 积压严重"| Degrade3["降级 3:同步下单模式<br/>绕过 MQ 直接写 DB"]
    Normal -->|"全面故障"| Degrade4["兜底:静态页面<br/>'活动已结束'"]
    style Degrade4 fill:#fcc,stroke:#c00

熔断机制

// Sentinel 限流 + 熔断
@SentinelResource(
    value = "seckill",
    blockHandler = "blockHandler",  // 限流/熔断时的处理
    fallback = "fallback"           // 异常时的处理
)
public SeckillResult doSeckill(long userId, long itemId) {
    // 正常秒杀逻辑
}

public SeckillResult blockHandler(long userId, long itemId, BlockException ex) {
    return SeckillResult.fail("系统繁忙,请稍后重试");
}

public SeckillResult fallback(long userId, long itemId, Throwable t) {
    return SeckillResult.fail("秒杀失败,请稍后重试");
}

兜底方案(Plan B)

缓存失效兜底

  • 本地缓存(Caffeine)+ 分布式缓存(Redis)双层
  • Redis 故障时,本地缓存提供服务(TTL 更短,数据可能稍旧)

DB 故障兜底

  • 关闭下单入口,展示友好错误页
  • 保留 Redis 中的库存数据,待 DB 恢复后补单

流量超预期兜底

  • 全局限流兜底(Nginx 层硬限制)
  • 自动扩容(K8s HPA)

监控体系

关键监控指标

层级指标告警阈值说明
流量QPS、TPS超过预期 150%防止流量超预期
性能TP99 响应时间> 500ms用户体验下限
成功率下单成功率< 95%业务核心指标
库存Redis 库存剩余= 0 时告警活动结束信号
队列MQ 消息堆积量> 10 万消费者处理能力不足
数据库慢 SQL 数量> 0及时发现性能问题
错误率5xx 错误率> 1%系统健康度

监控看板关键图表

  1. 流量漏斗图:各层拦截量,直观展示削峰效果
  2. 库存消耗曲线:库存随时间的消耗速率
  3. 响应时间分布:TP50/TP99/TP999
  4. MQ 消费延迟:消息从生产到消费的延迟

参考资料

  • 《如何设计一个秒杀系统》— 许令波(极客时间,ID: 461702917)
  • 《手把手带你搭建秒杀系统》— 志东(极客时间,ID: 1277675645)
  • 《高并发系统设计 40 问》— 唐扬(极客时间)
  • 《大型网站技术架构演进与性能优化》— 许令波
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论