设计一个高并发、高可用的秒杀系统,是分布式架构中的经典难题。它需要从前端到后端、从网络层到数据层进行全方位优化和防护。
你提到的关键词非常全面:前端防重、CDN、Nginx、网关、Ribbon、缓存、减库存、Sentinel、Seata、MQ —— 我们将围绕这些技术点,系统性地构建一个完整的秒杀架构设计方案。
🎯 一、秒杀系统的挑战
挑战 | 说明 |
---|---|
流量洪峰 | 瞬间流量可能是日常的百倍甚至千倍 |
超卖问题 | 库存不能为负,防止同一商品被多人抢到 |
数据一致性 | 减库存、生成订单、支付状态要一致 |
高并发处理 | 后端服务不能崩溃,响应要快 |
黄牛/刷单 | 需要防机器刷单、重复提交 |
🧱 二、整体架构图(逻辑分层)
用户 → CDN → Nginx(静态资源) → API Gateway(鉴权/限流) → 微服务集群
↓
Ribbon(客户端负载均衡)
↓
秒杀服务(缓存预热 + 防重 + 减库存)
↓
Redis(库存缓存) + MQ(异步下单) + DB(持久化)
↓
Sentinel(熔断降级) + Seata(分布式事务)
✅ 三、各层详细设计与优化策略
1️⃣ 前端层:防重点 + 静态化 + CDN 缓存
✅ 防重点(防重复提交)
- 按钮置灰:点击“立即抢购”后立即禁用按钮,防止多次点击。
- Token 机制:
- 用户进入秒杀页面前,先请求
/api/seckill/token
获取唯一令牌(Redis 存储,有效期5分钟)。 - 提交秒杀请求时必须携带该 token。
- 后端校验 token 是否有效,并使用 Lua 脚本原子删除 token,防止重放攻击。
- 用户进入秒杀页面前,先请求
✅ 页面静态化 + CDN 缓存
- 秒杀商品详情页提前生成为静态 HTML(如:
/seckill/1001.html
)。 - 所有静态资源(JS、CSS、图片)通过 CDN 分发,减轻源站压力。
- 动态数据(倒计时、剩余库存)通过 AJAX 异步加载。
✅ 倒计时前端控制
- 使用 JS 实现倒计时,避免频繁请求服务器。
- 只有倒计时结束才允许点击按钮。
2️⃣ 网络层:Nginx 负载均衡 + 静态资源代理
✅ Nginx 作用
- 静态资源代理:直接返回 HTML、JS、CSS,不打到后端服务。
- 负载均衡:将请求分发到多个网关实例。
- IP 限流:防止恶意刷请求。
深色版本
limit_req_zone $binary_remote_addr zone=seckill:10m rate=5r/s; location /seckill { limit_req zone=seckill burst=10 nodelay; proxy_pass http://gateway_servers; }
3️⃣ 网关层(Spring Cloud Gateway):统一入口 + 安全控制
✅ 核心功能
- 统一入口:所有请求经过网关。
- 权限校验:检查用户登录状态(JWT)。
- 限流降级:
- 使用 Sentinel 集成网关流控,按 API 路径限流(如
/seckill/do
每秒最多 1000 次)。 - 异常时返回友好提示,不穿透到后端。
- 使用 Sentinel 集成网关流控,按 API 路径限流(如
- 黑白名单:封禁频繁请求的 IP 或设备。
✅ 路由配置示例
spring:
cloud:
gateway:
routes:
- id: seckill-service
uri: lb://seckill-service
predicates:
- Path=/api/seckill/**
4️⃣ 客户端负载均衡:Ribbon / LoadBalancer
- 在微服务调用中(如订单服务调用库存服务),使用
@LoadBalanced
的RestTemplate
或FeignClient
。 - Ribbon 或 Spring Cloud LoadBalancer 实现客户端负载均衡,避免单点瓶颈。
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
5️⃣ 后端服务层:秒杀核心逻辑
✅ 服务拆分建议
seckill-service
:处理秒杀逻辑order-service
:创建订单inventory-service
:管理库存(可合并)
✅ 缓存预热(Cache Warm-up)
- 在秒杀开始前,提前将商品信息、库存数量加载到 Redis。
- 示例:
// 预热库存 redisTemplate.opsForValue().set("seckill:stock:1001", "100");
✅ 减库存策略(核心!)
方案选择:Redis 预减库存 + MQ 异步下单
步骤 | 说明 |
---|---|
1. 校验用户资格 | 是否登录、是否已抢过 |
2. Redis 减库存 | 使用 Lua 脚本原子操作 |
3. 成功则发 MQ | 发送“下单消息”到 RabbitMQ/Kafka |
4. 消费者创建订单 | 异步持久化到数据库 |
Lua 脚本示例(保证原子性):
local stock_key = KEYS[1]
local stock = tonumber(redis.call('get', stock_key))
if stock <= 0 then
return -1
end
redis.call('decr', stock_key)
return stock - 1
Java 调用:
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("seckill:stock:" + itemId),
null
);
if (result == -1) {
throw new RuntimeException("库存不足");
}
✅ 优点:高性能、避免超卖、解耦下单流程。
✅ 防黄牛 & 防刷机制
- 限流:基于用户 ID/IP/设备 ID 限流(Sentinel 或 Redis 计数器)。
- 答题验证:简单滑块或验证码,增加机器刷单成本。
- 黑名单机制:对异常行为用户加入 Redis 黑名单。
6️⃣ 消息队列(MQ):异步化 & 削峰填谷
✅ 使用场景
- 用户秒杀成功后,发送消息到 MQ:
{ "userId": "123", "itemId": "1001", "orderId": "ord_123" }
- 订单服务消费消息,创建订单、扣数据库库存、发通知。
✅ 优势
- 削峰:把瞬时高并发写请求变成平缓的消费流。
- 解耦:秒杀服务不直接调用订单服务。
- 可靠:MQ 支持持久化,防止消息丢失。
推荐:RabbitMQ(中小型)、Kafka(大型高吞吐)、RocketMQ(阿里系,金融级)
7️⃣ Sentinel:流量防护与熔断降级
✅ 作用
- 限流:控制
/seckill/do
接口 QPS,防止系统雪崩。 - 熔断:当库存服务异常时,自动熔断,返回“活动太火爆,请稍后再试”。
- 降级:异常时返回兜底数据,不抛错。
✅ 规则配置示例
- 资源名:
/api/seckill/do
- QPS 阈值:1000
- 流控模式:快速失败 / 排队等待
- 降级策略:异常比例 > 50% 则熔断 10s
8️⃣ Seata:分布式事务(保证一致性)
✅ 使用场景
- 当你需要强一致性时(如:减库存 + 创建订单 + 扣余额 必须同时成功或失败)。
- 使用 AT 模式 实现两阶段提交。
✅ 示例流程
- 用户秒杀成功 → 开启全局事务
@GlobalTransactional
- 调用
inventory-service
减库存 - 调用
order-service
创建订单 - 调用
account-service
扣余额 - 任一失败 → 全局回滚
⚠️ 注意:秒杀场景通常 不要求强一致性,可用最终一致性(MQ + 补偿)替代,性能更高。
📊 四、典型流程图(秒杀下单)
用户点击抢购
↓
网关层:鉴权 + 限流(Sentinel)
↓
秒杀服务:校验 token + Lua 脚本减 Redis 库存
↓ 是
减库存成功?───否──→ 返回“已抢光”
↓ 是
发送 MQ 消息(用户ID、商品ID)
↓
返回“抢购成功,请等待出票”
↓
MQ 消费者:创建订单、扣 DB 库存、发短信
🔐 五、安全与防刷总结
措施 | 说明 |
---|---|
Token + Lua | 防止重复提交 |
CDN + 静态页 | 抗住大部分无效请求 |
Nginx 限流 | 防止 DDOS |
网关鉴权 | 防止未登录用户访问 |
Redis 计数器 | 限制每个用户最多抢1次 |
滑块验证 | 增加机器识别成本 |
黑名单机制 | 封禁恶意 IP/设备 |
📈 六、性能优化建议
优化点 | 说明 |
---|---|
Redis 集群 | 提升缓存读写能力 |
本地缓存(Caffeine) | 缓存热点商品信息,减少 Redis 压力 |
数据库分库分表 | 订单表按 user_id 或 order_id 分片 |
异步日志 | 使用 Disruptor 或 MQ 写日志 |
JVM 调优 | 合理设置堆大小、GC 策略 |
✅ 七、是否需要 Seata?
场景 | 是否推荐 Seata |
---|---|
强一致性要求(如金融级) | ✅ 推荐 |
最终一致性可接受(大多数秒杀) | ❌ 不推荐,用 MQ + 补偿机制更高效 |
推荐:MQ + 本地事务表 + 定时对账 实现最终一致性,性能远高于 Seata。
✅ 总结:各组件作用一览表
组件 | 在秒杀系统中的作用 |
---|---|
前端防重 | 防止用户多次点击 |
CDN | 缓存静态页面,抗住80%流量 |
Nginx | 静态资源服务 + 负载均衡 + IP 限流 |
Gateway | 统一入口 + 鉴权 + 限流 |
Ribbon | 客户端负载均衡 |
Redis | 缓存预热 + 预减库存(核心) |
Lua 脚本 | 保证减库存原子性 |
MQ | 异步下单 + 削峰填谷 |
Sentinel | 限流、熔断、降级 |
Seata | 分布式事务(按需使用) |
我们来深入细化秒杀系统的核心四步流程:
1. 校验用户资格 → 2. Redis 减库存 → 3. 发送 MQ 消息 → 4. 消费者创建订单
这四步是秒杀系统的“黄金路径”,每一步都必须高并发、高可用、防超卖。下面我将从设计思路、代码实现、注意事项三个维度详细讲解。
✅ 第一步:校验用户资格(是否登录、是否已抢过)
🎯 目的
- 防止未登录用户参与秒杀
- 防止同一个用户重复抢购(一人一单)
🔐 实现方式
1. 是否登录?
- 使用 JWT 或 Session 校验用户身份。
- 网关层或 Controller 层拦截未登录请求。
java
深色版本
// 示例:从 ThreadLocal 或 SecurityContext 获取用户
User currentUser = UserContext.getCurrentUser();
if (currentUser == null) {
throw new BusinessException("请先登录");
}
2. 是否已抢过?(一人一单)
使用 Redis 记录用户是否已经成功抢到该商品。
java
深色版本
String userHasBoughtKey = "seckill:bought:" + itemId + ":" + userId;
Boolean hasBought = redisTemplate.hasKey(userHasBoughtKey);
if (Boolean.TRUE.equals(hasBought)) {
throw new BusinessException("您已抢购过此商品");
}
✅ 可选优化:设置过期时间,比如
redisTemplate.expire(userHasBoughtKey, 30, TimeUnit.MINUTES);
防止长期占用内存。
⚠️ 注意事项
- 这个判断要在“减库存”之前做,避免资源浪费。
- 如果允许“每人限购 N 件”,可以用
Redis incr
记录购买数量。
✅ 第二步:Redis 减库存(使用 Lua 脚本原子操作)
这是防止超卖的核心!必须保证“读库存 → 判断 → 减库存”是原子操作。
🧠 为什么用 Lua?
Redis 是单线程执行命令的,Lua 脚本在 Redis 中也是原子执行的,不会被其他请求打断。
📜 Lua 脚本(推荐)
lua
深色版本
-- KEYS[1] = 库存 key, ARGV[1] = 用户ID, ARGV[2] = 商品ID
local stock_key = KEYS[1]
local user_id = ARGV[1]
local item_id = ARGV[2]
local user_bought_key = "seckill:bought:" .. item_id .. ":" .. user_id
-- 1. 检查用户是否已抢过
if redis.call("EXISTS", user_bought_key) == 1 then
return -2 -- 已抢过
end
-- 2. 获取当前库存
local stock = tonumber(redis.call("GET", stock_key))
if not stock or stock <= 0 then
return -1 -- 无库存
end
-- 3. 原子减库存
redis.call("DECR", stock_key)
-- 4. 标记用户已抢购(防止重复提交)
redis.call("SET", user_bought_key, "1")
redis.call("EXPIRE", user_bought_key, 1800) -- 30分钟过期
return stock - 1 -- 返回剩余库存
💻 Java 调用代码(Spring Data Redis)
java
深色版本
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 加载 Lua 脚本
private DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 假设脚本内容已读取为 String 类型 luaScript
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
// 执行
List<String> keys = Collections.singletonList("seckill:stock:1001");
String userId = "123";
String itemId = "1001";
Long result = redisTemplate.execute(
redisScript,
keys,
userId, itemId
);
switch (result.intValue()) {
case -1:
throw new BusinessException("库存不足");
case -2:
throw new BusinessException("您已抢购过此商品");
default:
log.info("秒杀成功,剩余库存:{}", result);
// 走下一步:发 MQ
break;
}
⚠️ 注意事项
- Lua 脚本要尽量短小,避免阻塞 Redis。
user_bought
标记和库存 key 一起在 Lua 中处理,避免竞态条件。- 可以在 Lua 中加入“开始/结束时间”判断,防止提前或过期请求。
✅ 第三步:成功则发 MQ(发送“下单消息”)
减库存成功后,立即返回响应给用户,同时异步发送消息创建订单。
🎯 为什么异步?
- 减库存快(Redis 操作 < 1ms)
- 创建订单慢(DB 写入、日志、通知等)
- 异步化可以提升响应速度,避免用户等待
📦 消息内容设计
json
深色版本
{
"userId": "123",
"itemId": "1001",
"orderId": "order_123456789",
"timestamp": 1730000000000
}
✅ 建议:
orderId
在生产者端生成(UUID 或 Snowflake),避免消费者重复生成。
💬 Java 发送代码(RabbitMQ 示例)
java
深色版本
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendSeckillMessage(String userId, String itemId, String orderId) {
SeckillMessage message = new SeckillMessage();
message.setUserId(userId);
message.setItemId(itemId);
message.setOrderId(orderId);
message.setTimestamp(System.currentTimeMillis());
// 发送到秒杀订单队列
rabbitTemplate.convertAndSend("seckill.order.exchange", "seckill.create", message);
}
✅ 生产者要开启 Confirm 机制,确保消息不丢失。
⚠️ 注意事项
- 消息体要轻量,不要传大对象。
- 可以加一个“延迟队列”或“TTL”机制,防止用户抢到但长时间不支付。
✅ 第四步:消费者创建订单(异步持久化到数据库)
MQ 消费者负责最终一致性落地。
🧩 消费者逻辑
java
深色版本
@RabbitListener(queues = "seckill.order.queue")
public void handleSeckillOrder(SeckillMessage message, Channel channel) throws IOException {
try {
// 1. 检查订单是否已存在(幂等性)
if (orderService.orderExists(message.getOrderId())) {
log.warn("订单已存在,忽略重复消息: {}", message.getOrderId());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
// 2. 创建订单(本地事务)
Order order = new Order();
order.setId(message.getOrderId());
order.setUserId(message.getUserId());
order.setItemId(message.getItemId());
order.setStatus(OrderStatus.CREATED);
order.setCreateTime(new Date());
orderService.createOrder(order); // 包含 DB 持久化
// 3. 可选:发送短信/通知
notificationService.sendSuccess(message.getUserId(), message.getItemId());
// 4. 手动 ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("处理秒杀订单失败", e);
// 可以选择重试或进入死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
🔐 关键点:幂等性
- 同一个消息可能被重复消费(网络问题、ACK 失败)。
- 必须通过
orderId
先查库,避免重复创建订单。
⚠️ 注意事项
- 消费者要捕获异常,防止消息丢失。
- 建议使用 手动 ACK,确保处理成功才确认。
- 可以加监控:消息积压、消费延迟、失败率。
🔄 完整流程图(带异常处理)
深色版本
用户请求
↓
校验登录 + 是否已抢 → 否 → 返回“请登录”或“已抢过”
↓ 是
执行 Lua 脚本减库存 → 库存不足 → 返回“已抢光”
↓ 成功
生成 orderId,返回“抢购成功”
↓
发送 MQ 消息(异步)
↓
用户看到成功页面
←←←←←←←←←←←←←←←←
↑
消费者监听 MQ
↓
检查订单是否已存在(幂等)
↓
创建订单(DB 持久化)
↓
发送通知
↓
手动 ACK
✅ 总结:四步核心要点
步骤 | 核心技术 | 关键目标 |
---|---|---|
1. 校验资格 | Redis、JWT | 防未登录、防重复抢 |
2. 减库存 | Redis + Lua 脚本 | 原子性、防超卖 |
3. 发 MQ | RabbitMQ/Kafka | 异步化、削峰填谷 |
4. 创建订单 | 消费者 + 幂等处理 | 最终一致性、持久化 |
💡 额外建议
- 压测:上线前用 JMeter 做高并发压测。
- 降级预案:Redis 故障时,可降级到数据库 + 限流。
- 对账系统:每天定时核对“Redis 库存”和“DB 库存”是否一致。