⚡ 高并发架构 · 系统设计

秒杀系统设计全解析

从核心原理到生产级方案,一次性讲透秒杀系统的设计思路、常见问题与工程实践

📑 全文目录
01

秒杀系统是什么

📖 定义

秒杀系统是一种应对瞬时超高并发的电商场景系统,核心特征是在极短时间内(通常几秒到几分钟),大量用户争抢极少量商品。

10w+
瞬时QPS峰值
100:1
请求/库存比
秒级
活动持续时间
极低
实际成交率

🎯 秒杀 vs 普通电商

维度普通电商秒杀系统
并发量平稳,可预测瞬时暴增 100-1000 倍
库存充足极度稀缺(几十~几百件)
请求有效比高(大部分浏览→下单)极低(99%+ 请求无效)
核心矛盾功能丰富、体验好高可用、防超卖、防刷
缓存策略常规缓存加速多级缓存 + 本地缓存
数据库压力可通过读写分离缓解必须避免请求穿透到 DB
设计原则尽量让用户买到尽量让无效请求提前拦截

🏪 典型场景

🛒
电商限时抢购
双11、618、年货节
🎫
票务抢票
春运火车票、演唱会门票
🎁
营销活动
红包雨、优惠券抢领
02

核心挑战分析

🔴 高并发冲击

瞬时流量可达日常的百倍甚至千倍,系统可能在几秒内收到数十万请求

  • 网络带宽被打满
  • 连接池耗尽
  • 线程池排队
  • GC 压力暴增

🟠 超卖风险

库存只有 100 件却卖出了 120 件,这是最严重的业务事故

  • 并发读导致脏读
  • 扣减非原子操作
  • 缓存与DB不一致
  • 分布式环境竞态

🟣 恶意刷单

黄牛用脚本秒杀,正常用户根本抢不到

  • 机器人自动抢购
  • 分布式IP刷单
  • 撞库与账号盗用
  • 刷接口消耗资源

🔵 可用性保障

秒杀期间系统不能挂,挂了比卖超还严重

  • 单点故障雪崩
  • 依赖服务拖垮主链路
  • 缓存击穿/穿透
  • 数据库连接耗尽
⚠️ 本质矛盾:读多写少(99% 是查询,1% 是下单),但 1% 的写操作必须绝对正确(不能超卖)。核心设计思路就是层层拦截无效读请求,把写操作保护到极致
03

整体架构设计

🏗️ 经典分层架构

客户端层
App / H5 / 小程序
按钮置灰防重复
本地倒计时
CDN / Nginx
静态资源 CDN
Nginx 限流
活动页缓存
网关层
API Gateway
Token 校验
IP 限流 / 黑名单
服务层
秒杀服务(核心)
Redis 预扣库存
分布式锁
MQ 异步下单
数据层
MySQL(订单持久化)
Redis(库存缓存)
MQ(异步解耦)

🔄 秒杀核心请求流程

用户点击
秒杀按钮
前端校验
防重复 + 倒计时
网关限流
Token + IP黑名单
Redis 预扣
DECR 原子减库存
MQ 发消息
异步下单
DB 落单
订单持久化
✅ 关键思路:Redis 承接读压力,MQ 削峰填谷,MySQL 只处理有效订单。请求逐层递减,到 DB 层时仅剩真正需要处理的请求。
04

请求层层拦截

🔑 秒杀设计的黄金法则:将 99% 的无效请求在到达数据库之前拦截掉。每一层都是一个过滤器,越往后请求越少。

🛡️ 五层拦截体系

层级拦截手段拦截比例效果
L1 客户端 按钮置灰、倒计时、验证码、请求节流 ~30% 减少重复/无效点击
L2 CDN/Nginx 限流(令牌桶/漏桶)、静态资源CDN缓存 ~40% 流量在边缘被截断
L3 网关层 用户ID限流、IP限流、Token校验、黑名单 ~15% 识别并拒绝恶意请求
L4 服务层 Redis 库存预判、本地缓存标记、分布式锁 ~14% 无库存直接返回
L5 数据层 乐观锁/悲观锁、唯一索引、事务 ~1% 最终兜底防超卖

📱 前端拦截

  • 按钮置灰:点击后立即置灰 3-5 秒,防重复提交
  • 倒计时校验:未到秒杀时间按钮不可用
  • 图形验证码:秒杀前输入验证码,削峰+防机器人
  • 请求随机延迟:0~2s 随机延迟发出请求,避免同时涌入
  • 密码/答题:活动开启后弹出简单算术题

🌐 Nginx 限流配置

# 限制每个 IP 每秒 10 个请求 limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s; location /api/seckill { limit_req zone=seckill burst=20 nodelay; proxy_pass http://backend; } # 限制总并发连接数 limit_conn_zone $server_name zone=conn_limit:10m; limit_conn conn_limit 5000;
05

库存扣减方案

⚙️ 三种扣减方案对比

方案实现方式性能一致性复杂度推荐度
方案一 下单减库存(DB 直接扣) ❌ 低 ❌ 有超卖风险 ❌ 不推荐
方案二 付款减库存 ⚠️ 中 ❌ 可能卖不出 ⚠️ 有缺陷
方案三 预扣库存(Redis + DB 兜底) ✅ 高 ✅ 最终一致 ✅ 推荐

✅ 推荐方案:Redis 预扣 + DB 兜底

检查本地缓存
售罄标记
Redis DECR
原子预扣库存
结果 ≥ 0?
有库存继续
发 MQ 消息
异步创建订单
DB 扣减库存
乐观锁兜底
// Redis 预扣库存核心代码 public boolean deductStock(String itemId) { // 1. 本地缓存售罄标记,减少 Redis 访问 if (soldOutCache.getIfPresent(itemId) != null) { return false; } // 2. Redis 原子减库存(Lua 脚本保证原子性) Long remain = redisTemplate.execute(seckillScript, Collections.singletonList("seckill:stock:" + itemId)); // 3. 库存不足,设置本地售罄标记 if (remain < 0) { soldOutCache.put(itemId, true); return false; } // 4. 发送 MQ 消息异步下单 mqProducer.send(new OrderMessage(itemId, userId)); return true; }

📜 Redis Lua 原子扣减脚本

-- 秒杀 Lua 脚本:原子检查 + 扣减 local key = KEYS[1] -- seckill:stock:{itemId} local userId = ARGV[1] -- 用户ID(防重复购买) -- 检查是否已购买 if redis.call('sismember', key..':users', userId) == 1 then return -2 -- 已购买 end -- 检查并扣减库存 local stock = tonumber(redis.call('get', key)) if stock == nil or stock <= 0 then return -1 -- 无库存 end redis.call('decr', key) redis.call('sadd', key..':users', userId) return 1 -- 秒杀成功
✅ Lua 脚本在 Redis 中原子执行,检查 + 扣减 + 标记三步不可分割,彻底消除并发竞态。
06

超卖问题详解

❓ 超卖是怎么发生的

❌ 超卖场景

库存 = 1,两个请求同时到达:

  1. 请求A 读取库存 = 1 ✓
  2. 请求B 读取库存 = 1 ✓
  3. 请求A 扣减 → 库存 = 0
  4. 请求B 扣减 → 库存 = -1 💥

结果:卖出了 2 件,实际只有 1 件

VS

✅ 正确方案

使用原子操作 / 乐观锁:

  1. 请求A 原子 DECR → 库存 = 0 ✓
  2. 请求B 原子 DECR → 库存 = -1
  3. 请求B 检测到 < 0,回滚 INCR
  4. 请求B 返回"售罄"

结果:只卖 1 件,正确!

🔒 防超卖四重保障

第 1 重:Redis 原子操作
Lua 脚本保证「检查+扣减」原子性,单线程模型无竞态
第 2 重:数据库乐观锁
-- 乐观锁:带条件更新 UPDATE stock_table SET stock = stock - 1 WHERE item_id = 'ITEM001' AND stock > 0; -- 关键:stock > 0
第 3 重:唯一索引防重
订单表 (user_id, item_id) 建立唯一索引,同一用户不能重复购买
第 4 重:对账兜底
定时任务对账 Redis 预扣量与 DB 实际库存,发现异常自动补偿
07

高可用保障

🔄 熔断机制

当下游服务(如积分服务、库存服务)异常率超过阈值,自动熔断,直接返回降级结果

  • 熔断器三态:关闭 → 打开 → 半打开
  • 阈值:错误率 > 50% 或超时率 > 30%
  • 恢复:半开状态放少量请求探测
  • 工具:Sentinel / Hystrix / Resilience4j

⚡ 降级策略

系统压力过大时主动牺牲非核心功能,保全核心链路

降级级别降级内容
L1 轻度关闭推荐、评论等非核心功能
L2 中度静态化页面,跳过实时价格查询
L3 重度排队提示"活动火爆,请稍后再试"
L4 极端直接返回售罄,停止接收新请求

🛡️ 优雅降级示例

// 秒杀接口降级逻辑 public Result seckill(SeckillRequest req) { // 1. 检查系统负载,超阈值直接降级 if (systemOverload()) { return Result.busy("活动太火爆,请稍后再试"); } // 2. 核心路径:Redis → MQ → DB try { boolean success = redisDeduct(req); if (!success) return Result.soldOut(); sendMQ(req); return Result.queuing(); } catch (Exception e) { // 3. 异常降级:回滚 Redis 预扣 rollbackRedis(req); return Result.error("系统异常"); } }
08

缓存策略

🏗️ 三级缓存架构

层级存储内容命中率延迟
L1 本地缓存Caffeine / Guava售罄标记、活动配置~95%< 1ms
L2 分布式缓存Redis Cluster库存数量、用户购买标记~90%1-5ms
L3 数据库MySQL订单持久化、最终库存-10-50ms

💥 缓存穿透

大量请求查询不存在的数据,绕过缓存直达DB

方案:

  • 布隆过滤器拦截非法请求
  • 空值缓存(设短TTL如60s)
  • 请求参数校验前置

💥 缓存击穿

热点key过期瞬间,大量请求同时打到DB

方案:

  • 热点key永不过期(逻辑过期)
  • 互斥锁重建缓存
  • 提前预热缓存

🔑 缓存与DB一致性

❌ 先更新DB再删缓存

并发场景下可能出现:

  1. 请求A 更新 DB → 库存=99
  2. 请求B 读缓存 → 库存=100(旧值)
  3. 请求B 将旧值写回缓存
  4. 请求A 删缓存 → 但B已写回旧值

缓存与DB不一致!

VS

✅ 推荐方案

延迟双删 + 消息队列兜底:

  1. 先删缓存
  2. 再更新DB
  3. 延迟500ms再删一次缓存
  4. 发MQ消息,消费者再删一次

最终一致性保障

09

消息队列

📬 MQ 在秒杀中的三大作用

🏔️
削峰填谷
10w QPS → MQ → 消费者按DB能力匀速消费(如 1k QPS)
🔗
异步解耦
下单 → 发MQ → 立即返回 → 消费者异步创建订单+扣积分+发短信
📢
广播通知
秒杀结果广播到各子系统:订单、物流、通知等

⏱️ 异步下单流程

用户秒杀
同步接口
Redis 预扣
库存成功
发 MQ 消息
立即返回"排队中"
消费者处理
创建订单+扣DB库存
WebSocket
推送结果给用户
💡 用户视角:点击秒杀 → 页面显示"排队中" → 1-3 秒后收到结果通知。体验上几乎无感,但后端压力被 MQ 削平。

🤔 MQ 选型对比

维度RocketMQKafkaRabbitMQ
吞吐量⭐⭐⭐⭐⭐ (10w+)⭐⭐⭐⭐⭐ (100w+)⭐⭐⭐ (万级)
延迟ms 级ms 级μs 级
可靠性极高(同步刷盘+同步双写)高(副本机制)高(镜像队列)
事务消息✅ 原生支持❌ 需自行实现❌ 不支持
顺序消费✅ 队列级别有序✅ Partition 有序⚠️ 单Consumer有序
秒杀推荐✅ 首选(阿里出品,专为电商)✅ 备选(超大规模)❌ 吞吐不够
10

分布式锁

🔐 分布式锁三大实现

方案实现性能可靠性适用场景
Redis SETNX + 过期时间 ⭐⭐⭐⭐⭐ ⚠️ 需防锁续期失败 秒杀首选
ZooKeeper 临时顺序节点 ⭐⭐⭐ ⭐⭐⭐⭐⭐ 强一致性要求高
MySQL SELECT FOR UPDATE ⭐⭐ ⭐⭐⭐⭐ 兜底方案

🔑 Redis 分布式锁(Redisson)

// 使用 Redisson 实现分布式锁 RLock lock = redissonClient.getLock("seckill:lock:" + itemId); try { // 尝试加锁,最多等待 100ms,锁自动释放时间 3s if (lock.tryLock(100, 3000, TimeUnit.MILLISECONDS)) { try { // 执行秒杀业务逻辑 doSeckill(itemId, userId); } finally { lock.unlock(); } } else { // 获取锁失败,快速返回 return Result.busy("系统繁忙"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
⚠️ Redisson 内置看门狗(Watchdog)机制:默认 30s 锁过期,每 10s 自动续期。避免业务未执行完锁就过期。

⚠️ 分布式锁注意事项

  • 必须设过期时间:防死锁,持有锁的节点宕机后锁能自动释放
  • 加锁+过期必须原子:用 SET key value NX EX 命令,不能分两步
  • 释放锁必须校验:只能释放自己加的锁(用 UUID 标识),防止误删
  • 秒杀场景慎用分布式锁:锁是串行化操作,会严重限制吞吐。优先用 Redis Lua 原子操作
11

限流与降级

📊 四种限流算法

算法原理优点缺点适用场景
固定窗口 固定时间窗口内计数 实现简单 窗口边界突刺 简单限流
滑动窗口 窗口平滑滑动 解决边界突刺 内存开销稍大 精确限流
漏桶算法 恒定速率出水 流量平滑 无法应对突发 保护DB
令牌桶 恒定速率放令牌 允许合理突发 实现稍复杂 秒杀首选 ⭐

🪣 令牌桶算法图解

令牌桶 匀速放入令牌 请求取令牌 桶满则丢弃新令牌 | 无令牌则拒绝请求
  • 系统以恒定速率向桶中放令牌
  • 每个请求需取走一个令牌
  • 桶满则丢弃多余令牌
  • 桶空则拒绝/排队请求
  • 允许突发:桶中有积累令牌时可瞬间处理

🛡️ 多维度限流策略

维度限流规则目的
全局 QPS10w/s保护系统总容量
单用户5次/分钟防刷+公平
单 IP100次/秒防恶意攻击
单商品库存×3超卖保护
接口级别根据容量设定精细控制
12

数据库优化

🗄️ MySQL 优化要点

📝 表设计优化

  • 秒杀商品表独立:不与普通商品共用,避免锁竞争
  • 订单表水平分表:按 user_id 或 order_id 分表
  • 唯一索引:(user_id, item_id) 防重复下单
  • 字段精简:秒杀订单只存必要字段

⚡ SQL 优化

  • 乐观锁更新:UPDATE ... WHERE stock > 0
  • 避免复杂JOIN:秒杀链路单表操作为主
  • 批量插入:MQ 消费者批量创建订单
  • 读写分离:查询走从库,写入走主库

🔄 乐观锁 vs 悲观锁

✅ 乐观锁(推荐)

-- 带版本号的乐观锁 UPDATE stock SET stock = stock - 1, version = version + 1 WHERE item_id = 'ITEM001' AND version = 5;
  • 不阻塞读操作
  • 高并发性能好
  • 适合读多写少
  • 冲突时需重试
VS

⚠️ 悲观锁

-- 排他锁 SELECT * FROM stock WHERE item_id = 'ITEM001' FOR UPDATE;
  • 强一致性
  • 串行化严重
  • 锁等待超时
  • 秒杀场景不推荐
13

防刷与安全

🛡️ 防刷六重防护

层级手段原理防住什么
L1图形验证码/滑块人机识别,增加操作成本简单脚本
L2短信/邮箱验证绑定真实身份批量账号
L3IP 频次限制单IP单位时间请求数上限同IP多号
L4设备指纹识别同一设备多账号设备刷单
L5风控规则引擎行为特征分析+评分高级黄牛
L6动态令牌(Token)秒杀URL动态化,防直接调用接口盗刷

🔐 动态秒杀URL

// 秒杀接口动态化:URL 中包含动态 Token // 1. 获取秒杀 Token(秒杀开始前调用) GET /api/seckill/token?itemId=ITEM001 // 2. 服务端生成动态 Token String token = MD5(itemId + userId + salt + currentTimeWindow); redis.set("seckill:token:" + token, userId, 60s); // 60秒有效 // 3. 秒杀请求必须携带 Token POST /api/seckill/{dynamicToken} // 服务端校验 Token 合法性,非法直接拒绝
✅ 效果:攻击者无法提前知道秒杀 URL,即使抓包也无法复用(Token 有时效 + 用户绑定)。
14

订单处理

📋 订单状态机

创建订单
待支付
支付成功
已支付
发货
已发货
确认收货
已完成
⏰ 超时未支付 → 自动取消(回滚库存)
❌ 用户取消 → 已取消(回滚库存)
🔄 退款 → 已退款(回滚库存)

⏰ 超时取消方案

方案一:延迟消息(推荐)

// 创建订单时发送延迟消息 Message msg = new Message( "ORDER_TIMEOUT_TOPIC", orderId.getBytes() ); msg.setDelayTimeLevel(16); // 30分钟 producer.send(msg); // 消费者:超时检查并取消 if (order.status == UNPAID) { cancelOrder(orderId); rollbackStock(itemId); }

方案二:定时扫描

// 每 1 分钟扫描超时订单 @Scheduled(fixedRate = 60000) public void scanTimeoutOrders() { List<Order> orders = orderMapper .selectByStatusAndTime( UNPAID, before30Min ); orders.forEach(o -> { cancelOrder(o); rollbackStock(o.getItemId()); }); }
15

全链路压测

🧪 压测核心指标

QPS
每秒请求数
RT
响应时间 P99
SLA
可用性 99.99%
Error%
错误率 < 0.1%

📋 压测 Checklist

  • Redis 单节点/集群压测(GET/SET/Lua)
  • MQ 发送/消费延迟压测
  • MySQL 单表/分表写入压测
  • 网关限流阈值验证
  • 接口幂等性验证
  • 缓存穿透/击穿模拟
  • 超卖场景并发验证(100并发抢10件)
  • 降级开关有效性验证
  • 熔断恢复机制验证
  • Redis/MySQL 故障切换演练
  • 网络抖动/分区容错模拟
  • 全链路压测(线上影子表)

🔧 压测工具推荐

工具类型特点适用场景
JMeterGUI压测功能全面、插件丰富接口压测、场景编排
wrk / wrk2命令行压测轻量高效、支持恒定吞吐快速基准测试
Gatling代码化压测Scala DSL、报告美观持续集成压测
LocustPython压测代码即脚本、Web UI灵活场景编写
16

面试高频题

Q1:如何防止超卖?

四重保障:

  1. Redis Lua 原子扣减:检查+扣减+标记三合一原子操作
  2. DB 乐观锁:UPDATE ... WHERE stock > 0,兜底防超卖
  3. 唯一索引:(user_id, item_id) 防重复下单
  4. 对账补偿:定时对账 Redis 与 DB 数据一致性

核心:Redis 做性能防线,DB 做正确性兜底。

Q2:秒杀接口如何防止恶意刷单?
  1. 动态URL:秒杀接口地址动态生成,含加密Token
  2. 验证码:秒杀前需通过人机验证
  3. 多维度限流:用户维度 + IP维度 + 设备维度
  4. 风控系统:行为分析 + 规则引擎识别异常
  5. 设备指纹:识别同一设备多账号行为
Q3:Redis 挂了怎么办?
  1. Redis 集群:Redis Cluster 多主多从,自动故障转移
  2. 本地缓存兜底:Caffeine 做二级缓存,Redis 不可用时读取本地
  3. 降级策略:Redis 不可用 → 降级为 DB 查询(限流+排队)
  4. 快速恢复:提前预热数据,重启后快速加载库存到 Redis

生产环境 Redis 至少 6 节点集群(3主3从),RDB+AOF 持久化。

Q4:消息队列消息丢了怎么办?
  1. 生产端:同步发送 + 失败重试 + 本地消息表
  2. MQ 端:同步刷盘 + 主从同步复制
  3. 消费端:手动 ACK,处理成功后才确认
  4. 兜底:定时扫描本地消息表,重新发送未确认消息

RocketMQ 的事务消息是最佳方案,保证本地事务与消息发送的原子性。

Q5:秒杀系统的性能目标是多少?如何评估?
指标目标值说明
峰值 QPS10w+根据业务预估,预留 2 倍 buffer
RT P99< 200ms99% 请求 200ms 内响应
可用性99.99%全年不可用 < 52.6 分钟
超卖率0%绝对不允许超卖
错误率< 0.1%非业务拒绝类的系统错误

评估方法:全链路压测 + 线上影子表验证 + 容量规划(机器数 = 峰值QPS / 单机QPS × 冗余系数)

Q6:如何设计一个支持 100w 并发的秒杀系统?

核心架构演进:

  1. CDN 边缘计算:静态页面 + 边缘限流,拦截 60%+ 请求
  2. 多级缓存:本地缓存 → Redis 集群 → DB,逐层递减
  3. Redis 集群横向扩展:商品分片到不同 Redis 节点
  4. Kafka/RocketMQ 分区:按 itemId 分区,并行消费
  5. DB 分库分表:按 user_id 分库,按 order_id 分表
  6. 微服务拆分:秒杀服务独立部署,不影响主站
  7. K8s 弹性伸缩:秒杀前自动扩容,结束后缩容

关键:没有银弹,100w 并发靠的是层层拦截 + 水平扩展 + 异步削峰的组合拳。

Q7:如何保证幂等性?

幂等性 = 同一操作执行多次结果相同

  1. 唯一索引:(user_id, item_id) 数据库唯一约束
  2. Token 机制:服务端发放一次性 Token,用后即废
  3. Redis SETNX:SETNX order:userId:itemId 1,存在则拒绝
  4. 乐观锁版本号:UPDATE ... WHERE version = ?
  5. 状态机约束:只有特定状态才能转换
Q8:库存回滚场景有哪些?
  1. 支付超时:30分钟未支付,自动取消订单,Redis INCR 回库
  2. 用户取消:主动取消订单,回滚库存
  3. 系统异常:MQ 消费失败,Redis 预扣回滚
  4. 退款:售后退款成功,库存回补
  5. 风控拦截:下单后被风控识别为刷单,取消并回滚
// 库存回滚核心逻辑 public void rollbackStock(String itemId, String userId) { // 1. Redis 回滚库存 redis.incr("seckill:stock:" + itemId); // 2. 移除用户购买标记 redis.srem("seckill:stock:" + itemId + ":users", userId); // 3. 清除售罄缓存标记 soldOutCache.invalidate(itemId); }

核心思想总结

🎯 四大设计原则

  • 层层拦截:99% 请求在到达 DB 前被截断
  • 异步削峰:MQ 把峰值流量拉平为平稳流量
  • 最终一致:Redis 快速响应 + DB 兜底保正确
  • 兜底降级:每个环节都有降级和容错方案

⚡ 一句话总结

秒杀系统的本质是用空间换时间、用异步换同步、用缓存换DB

不是让所有请求都成功,而是让合法请求尽可能成功,让非法请求尽早失败

不是追求强一致性,而是保证最终一致性,用对账和补偿兜底