一句话总结
秒杀系统核心挑战:高并发(瞬时流量洪峰)、超卖(库存扣减一致性)、恶意请求。分层限流:前端(按钮置灰+验证码)→ 网关(令牌桶限流)→ 应用层(Redis预减库存+Lua原子操作)→ 消息队列(异步下单削峰)→ 数据库(最终扣减)。核心口诀:前端防重、网关限流、Redis减库存、MQ削峰、DB最终一致。
初级理解
秒杀系统架构分层
# 秒杀请求链路
# 用户 → CDN(静态页面)→ Nginx(限流)→ 秒杀服务
# → Redis(预减库存)→ MQ(异步下单)→ DB(扣减库存)
# 各层职责
# 1. 前端:按钮防重、验证码、静态化
# 2. CDN:缓存静态页面,减少源站压力
# 3. Nginx:令牌桶限流,拒绝多余请求
# 4. Redis:预减库存(Lua脚本原子操作)
# 5. MQ:异步下单,削峰填谷
# 6. DB:最终库存扣减,保证一致性
超卖问题
# ❌ 错误做法(有并发问题)
# 1. 查库存: SELECT stock FROM product WHERE id=1 # stock=1
# 2. 判断: if stock > 0
# 3. 扣减: UPDATE product SET stock=stock-1 WHERE id=1
# 问题:两个线程同时查到 stock=1,都执行扣减 → 超卖!
# ✅ 正确做法1:数据库行锁
UPDATE product SET stock = stock - 1
WHERE id = 1 AND stock > 0;
# 利用 InnoDB 行锁保证原子性
# ✅ 正确做法2:Redis Lua 脚本
local stock = redis.call('get', KEYS[1])
if tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1 # 秒杀成功
end
return 0 # 库存不足
一句话总结:超卖用 Redis Lua 原子操作或 DB 行锁(WHERE stock > 0)。
中级深入
Redis 预减库存 + Lua 脚本
# Lua 脚本(原子操作)
local product_id = KEYS[1]
local user_id = ARGV[1]
local stock_key = "seckill:stock:" .. product_id
local bought_key = "seckill:bought:" .. product_id
# 1. 检查是否已购买(防重复)
if redis.call('sismember', bought_key, user_id) == 1 then
return -1 # 已购买
end
# 2. 检查库存
local stock = redis.call('get', stock_key)
if not stock or tonumber(stock) <= 0 then
return 0 # 库存不足
end
# 3. 扣减库存 + 记录用户
redis.call('decr', stock_key)
redis.call('sadd', bought_key, user_id)
return 1 # 秒杀成功
消息队列异步下单
# 秒杀成功 → 发送MQ消息 → 异步创建订单
# 为什么用MQ?
# 1. 削峰:DB 处理能力有限,MQ 缓冲请求
# 2. 解耦:秒杀服务和订单服务解耦
# 3. 异步:用户快速拿到结果,后台慢慢处理
# 流程
# 1. Redis 扣减成功 → 发送 MQ 消息
# 2. 返回用户"排队中"
# 3. 订单服务消费 MQ → 创建订单
# 4. 用户轮询或 WebSocket 通知结果
中级要点:Redis Lua 原子减库存+防重复;MQ 异步下单削峰解耦。
高级拓展
限流策略
# 1. 前端限流
# - 按钮置灰(点击后禁用 5 秒)
# - 验证码(滑块/图形验证码)
# - 答题(秒杀前答题,过滤脚本)
# 2. Nginx 限流
# 令牌桶算法
limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
location /seckill {
limit_req zone=seckill burst=20 nodelay;
}
# 3. 应用层限流
# Sentinel/Guava RateLimiter
RateLimiter limiter = RateLimiter.create(1000); # 1000 QPS
if (!limiter.tryAcquire()) {
return "系统繁忙,请稍后再试";
}
数据一致性保证
# 最终一致性方案
# 1. Redis 预减库存(快速返回)
# 2. MQ 异步下单
# 3. DB 扣减库存(WHERE stock > 0)
# 4. 定时对账:Redis 库存 + DB 已售 = 总库存
# 异常处理
# - MQ 消费失败 → 重试 + 死信队列
# - DB 扣减失败 → 回滚 Redis 库存
# - 订单超时未支付 → 定时取消 + 回补库存
实战场景
场景:秒杀接口核心代码
@RestController
public class SeckillController {
@Autowired
private RedisTemplate redis;
@Autowired
private RocketMQTemplate mq;
@PostMapping("/seckill")
public Result seckill(Long productId, Long userId) {
# 1. Redis Lua 脚本预减库存
String lua = "local stock = redis.call('get', KEYS[1])\n" +
"if stock and tonumber(stock) > 0 then\n" +
" redis.call('decr', KEYS[1])\n" +
" return 1\n" +
"end\n" +
"return 0";
Long result = redis.execute(
new DefaultRedisScript<>(lua, Long.class),
List.of("seckill:stock:" + productId));
if (result == 0) return Result.fail("已售罄");
# 2. 发送 MQ 异步下单
mq.send("seckill-order", new SeckillMsg(productId, userId));
# 3. 返回排队中
return Result.ok("排队中,请稍后查看结果");
}
}
面试模拟
面试官:秒杀系统怎么防止超卖?
你:三层防护:1)Redis Lua 脚本原子预减库存(单线程,天然原子);2)DB 用行锁 UPDATE ... WHERE stock > 0;3)用户维度防重(Redis Set 记录已购买用户)。Redis 预减库存是核心,99% 的请求在 Redis 层就返回了。
面试官:秒杀系统怎么应对高并发?
你:分层限流:前端按钮防重+验证码 → CDN 静态化 → Nginx 令牌桶限流 → Redis 预减库存(扛住大部分流量)→ MQ 异步下单削峰 → DB 最终扣减。核心思路是尽量把请求挡在上游,减少到达 DB 的请求量。