本文目录导读:

- 核心思想:流量漏斗 + 层层过滤 + 最终一致性
- 第一部分:分布式限流(保护系统入口)
- 第二部分:防超卖(核心库存保护)
- 第三部分:流量削峰与最终一致性(保证不超卖的关键落地)
- 第四部分:完整流程总结(伪代码)
- 第五部分:关键点检查清单
这是一个非常经典且高难度的分布式系统设计问题。秒杀系统的核心挑战在于:短时间内海量并发请求(读和写)冲击单一热点资源(库存),导致数据库崩溃、库存超卖、用户响应缓慢。
针对“分布式限流”和“防超卖”这两个关键点,以下是经过生产验证的完整解决方案架构。
核心思想:流量漏斗 + 层层过滤 + 最终一致性
把请求想象成水流,从外到内逐层削减,最终只有极少量的请求能触达数据库。
graph TD
subgraph 客户端层
A[用户请求] --> B[CDN/浏览器静态化]
end
subgraph 接入层 (限流与防刷)
B --> C[Nginx/LVS]
C --> D{API网关/负载均衡}
D -- 令牌桶/漏桶 --> E[Nginx Lua限流模块]
end
subgraph 应用与缓存层 (核心过滤与库存扣减)
E --> F[Redis Cluster]
F -- 库存预扣/本地标记 -->
G[秒杀服务集群]
G -- 抢到资格 -->
H[消息队列 MQ]
end
subgraph 数据持久层 (异步落库)
H -- 异步/串行消费 -->
I[数据库 DB 事务]
I --> J[订单/库存持久化]
end
style F fill:#f9f,stroke:#333,stroke-width:2px
style H fill:#bbf,stroke:#333,stroke-width:2px
第一部分:分布式限流(保护系统入口)
目标是挡住大部分无效或过量的请求,防止系统被冲垮。
前端与网关限流(第一道防线)
- 动静分离:秒杀页面静态化(HTML/CSS/JS)、CDN加速,用户点击按钮前,页面静态部分不消耗服务器资源。
- 按钮防抖:点击后立即置灰,防止用户频繁点击。
- Nginx/Lua级别限流(最外层,性能最高):
- IP限流:同一IP每秒最多允许N次请求。
- URL限流:针对“/seckill/do”接口做全局限流。
- 连接数限制:限制Nginx到后端服务的最大连接数。
应用层分布式限流(第二道防线)
-
令牌桶算法(推荐):
-
原理:以固定速率向桶中放入令牌,请求必须拿到令牌才能通过,允许突发流量,但总体速率可控。
-
实现:使用 Redis + Lua脚本(原子性)实现分布式令牌桶,定义一个key,hash结构存储
last_refresh_time和tokens。 -
代码示例(Lua脚本简化逻辑):
-- 获取令牌逻辑 local key = KEYS[1] local rate = tonumber(ARGV[1]) -- 每秒速率,1000 local burst = tonumber(ARGV[2])-- 桶容量 local now = redis.call('TIME')[1] local token_info = redis.call('HMGET', key, 'last_time', 'tokens') local last_time = tonumber(token_info[1]) or now local tokens = tonumber(token_info[2]) or burst -- 根据时间差计算应添加的令牌 local delta = math.max(0, now - last_time) tokens = math.min(burst, tokens + delta * rate) if tokens >= 1 then redis.call('HMSET', key, 'last_time', now, 'tokens', tokens - 1) return 1 -- 限流通过 else return 0 -- 限流拒绝 end
-
第二部分:防超卖(核心库存保护)
核心原则:将库存扣减操作提前到内存/缓存层,且必须保证原子性。
Redis 原子扣减库存(最重要的一步)
- 为什么不用数据库直接扣?:数据库行锁会导致性能瓶颈,无法支撑高并发。
- 操作方式:使用 Redis 的
DECR命令或 Lua脚本 进行库存扣减。- Lua脚本 保证整个操作(查库存、判断、扣减)的原子性,避免并发问题。
- 预判库存:先读库存,如果小于等于0,直接返回“已售罄”,避免无效的
DECR操作。-- Redis Lua 扣减库存脚本 -- KEYS[1]: 商品库存的Key (stock:1001) -- ARGV[1]: 购买数量 (通常为1) local stock = redis.call('GET', KEYS[1]) if not stock or tonumber(stock) <= 0 then return 0 -- 库存不足 end if tonumber(stock) - tonumber(ARGV[1]) < 0 then return 0 -- 库存不足 end return redis.call('DECRBY', KEYS[1], ARGV[1]) -- 返回剩余库存
本地缓存标记(为 Redis 减负)
- 问题:高并发下,即使使用Redis,大量请求查询“商品是否售罄”也会给Redis带来压力。
- 解决方案:
- 当从Redis中读取到某个商品的库存为0(或已被标记售罄)时,将状态写入秒杀服务本地内存(如Caffeine、Guava Cache)。
- 后续请求直接检查本地缓存,若已售罄,不再请求Redis,直接返回失败。
- 注意:需要设置一个较短的过期时间(如10秒),并配合一个后台任务定期检查Redis库存,防止库存恢复(如订单取消)后本地缓存未更新。
“一人一单”严格限制(结合用户维度)
- 问题:黄牛用多个账号/IP刷单。
- 技术实现:
- 基于Redis Set:使用商品ID+用户ID作为key,维护一个已购买成功的用户ID集合。
- 基于布隆过滤器:在秒杀开始前,将历史购买记录加载到布隆过滤器中,快速拦截重复购买。
第三部分:流量削峰与最终一致性(保证不超卖的关键落地)
异步下单(消息队列MQ)
- 为什么需要:扣减库存后,不能直接写入数据库,如果直接写入,高并发写入会导致数据库死锁或Connection池耗尽。
- 流程:
- Redis扣减库存成功。
- 将“用户ID、商品ID、数量、秒杀资格Token”封装成消息,发送到 MQ(如 RocketMQ/Kafka)。
- 立即返回给用户“正在排队中...”或“秒杀成功,请等待订单生成”。
- 后端服务异步消费MQ消息,单线程/串行化地处理数据库写操作。
- 防重复消费:数据库订单表中建立唯一索引(用户ID + 商品ID + 活动ID),确保同一用户不会生成两个订单。
数据库层兜底(乐观锁)
- 即使异步落库,最后一步写入数据库时仍需防御。
- 更新SQL:
UPDATE stock SET version = version + 1, num = num - 1 WHERE id = ? AND version = ? and num > 0 - 如果更新影响行数为0,说明库存被其他线程更新了,该订单作废(回滚Redis库存)。
第四部分:完整流程总结(伪代码)
// 1. 入口:接入层限流 (令牌桶)
if (!rateLimiter.tryAcquire()) {
return "请求过于频繁,请稍后重试";
}
// 2. 业务层:检查用户黑名单,一人一单,防刷 (布隆过滤器)
if (bloomFilter.mightContain(userId + productId)) {
return "您已经参与过该活动";
}
// 3. 本地缓存检查售罄 (JVM内存)
if (localCache.isSoldOut(productId)) {
return "商品已售罄";
}
// 4. Redis Lua 脚本原子扣减库存
Long stockRemaining = redisTemplate.execute(seckillLuaScript, stockKey, 1);
if (stockRemaining == null || stockRemaining < 0) {
// 扣减失败,标记本地缓存售罄
localCache.markSoldOut(productId, 10, TimeUnit.SECONDS);
return "商品已被抢光";
}
// 5. 扣减成功,发送MQ消息,异步落单 (加购标记,防重复)
orderMessage = new OrderMessage(userId, productId, token);
mqProducer.send(orderMessage);
// 6. 返回成功给用户
return "恭喜你,秒杀成功!正在处理订单...";
// --- 异步消费者 ---
@RocketMQMessageListener(topic = "seckill_order")
public void onMessage(OrderMessage msg) {
try {
// 7. 数据库防重复插入 (唯一索引)
// INSERT INTO order (user_id, product_id,...) VALUES (?,?,...)
// 如果唯一索引冲突,直接 return (幂等)
// 8. 数据库乐观锁扣减最终库存
int rows = stockMapper.updateStockByVersion(productId, version);
if (rows == 0) {
// 乐观锁失败,库存被改 (理论上不应该发生,或极少发生)
// 补偿:修改订单状态为“失败”,回滚Redis库存
redisTemplate.opsForValue().increment(stockKey, 1);
return;
}
// 9. 创建订单成功
} catch (Exception e) {
// 10. 重试/死信队列处理
}
}
第五部分:关键点检查清单
| 环节 | 技术手段 | 目标 |
|---|---|---|
| 前端 | 按钮置灰、静态化、CDN | 减少无效请求 |
| 网关 | Nginx Lua 限流(IP/URL) | 防刷、流量整形 |
| 应用层 | 分布式令牌桶(Redis+Lua) | 控制总并发数 |
| 库存层 | Redis Lua 原子扣减 | 严格防止超卖 |
| 状态缓存 | 本地缓存(Caffeine)标记售罄 | 减轻Redis压力 |
| 一人一单 | Redis Set / 布隆过滤器 | 防黄牛重复购买 |
| 削峰 | 消息队列(RocketMQ/Kafka) | 异步落库,保护DB |
| 最终一致 | 数据库乐观锁 + 唯一索引 | 兜底,防重复 |
通过以上“层层过滤、异步削峰、原子扣减”的组合拳,就能构建一个稳定、不超卖的分布式秒杀系统。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播