30分钟课程:电商秒杀系统实战(流量削峰 + 库存预热 + 请求排队)
课程目标
- 掌握秒杀系统核心架构设计:流量削峰、库存预热、请求排队。
- 实现基于 Redis 的令牌桶限流与库存原子扣减。
- 通过 Redis List 或 Kafka 实现高并发请求的异步处理。
课程内容与时间分配
0~5分钟:课程概述
业务场景与挑战
- 瞬时高并发:万人抢购导致服务崩溃、数据库击穿。
- 资源竞争:库存超卖、订单重复提交。
- 核心目标:保障系统高可用、数据一致性、用户体验流畅。
技术方案
- 流量削峰:令牌桶限流控制入口请求量。
- 库存预热:活动开始前将库存加载至 Redis,避免直接访问数据库。
- 请求排队:Redis List 或 Kafka 缓冲请求,异步处理订单。
5~10分钟:技术难点与核心问题
- 令牌桶算法实现
- 分布式环境下如何保证限流计数原子性?
- 库存预热与扣减
- Redis 预减库存时如何避免超卖?
- 请求队列可靠性
- 如何防止消息丢失或重复消费?
- 数据一致性
- 订单创建、库存扣减、支付状态如何保证最终一致?
10~25分钟:解决方案与代码实战
1. 流量削峰:Redis令牌桶限流(10~15分钟)
令牌桶原理
- 每秒发放固定数量令牌(如 1000 个),请求获取令牌后才能进入系统。
Lua脚本实现原子操作
-- KEYS[1]: 令牌桶键(如 limit:seckill)
-- ARGV[1]: 桶容量
-- ARGV[2]: 令牌生成速率(每秒)
-- ARGV[3]: 当前时间戳(秒)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 1. 初始化桶数据
local last_time = tonumber(redis.call('hget', key, 'last_time')) or now
local tokens = tonumber(redis.call('hget', key, 'tokens')) or capacity
-- 2. 计算新增令牌数
local new_tokens = math.floor((now - last_time) * rate)
tokens = math.min(capacity, tokens + new_tokens)
last_time = now
-- 3. 尝试获取令牌
if tokens >= 1 then
tokens = tokens - 1
redis.call('hset', key, 'last_time', last_time)
redis.call('hset', key, 'tokens', tokens)
return 1 -- 获取成功
else
return 0 -- 令牌不足
end
Java调用代码
@Service
public class RateLimiterService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LUA_SCRIPT = "上述 Lua 脚本内容";
public boolean tryAcquire(String key, int capacity, int rate) {
long now = System.currentTimeMillis() / 1000;
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(key),
String.valueOf(capacity), String.valueOf(rate), String.valueOf(now));
return result != null && result == 1;
}
}
2. 库存预热:Redis原子扣减(15~20分钟)
库存预热与扣减流程
- 预热库存:活动开始前将库存从数据库加载到 Redis。
- 原子扣减:使用 Lua 脚本保证查询与扣减的原子性。
Lua脚本(扣减库存)
-- KEYS[1]: 库存键(如 stock:item_1001)
-- ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return redis.call('GET', KEYS[1]) -- 返回剩余库存
else
return -1 -- 库存不足
end
Java代码:预热库存
public void preheatStock(String itemId, int stock) {
redisTemplate.opsForValue().set("stock:" + itemId, String.valueOf(stock));
}
public boolean deductStock(String itemId, int quantity) {
String script = "上述 Lua 脚本内容";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList("stock:" + itemId),
String.valueOf(quantity));
return result != null && result >= 0;
}
3. 请求排队:Redis List或Kafka(20~25分钟)
方案1:Redis List队列
// 生产者:接收请求并存入队列
public void enqueueRequest(String itemId, String userId) {
redisTemplate.opsForList().leftPush("queue:seckill:" + itemId, userId);
}
// 消费者:异步处理队列
@Scheduled(fixedDelay = 100)
public void processQueue() {
String userId = redisTemplate.opsForList().rightPop("queue:seckill:item_1001", 1, TimeUnit.SECONDS);
if (userId != null) {
orderService.createOrder(userId, "item_1001");
}
}
方案2:Kafka异步处理
// 生产者发送消息
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendSeckillRequest(String userId, String itemId) {
kafkaTemplate.send("seckill_requests", userId + ":" + itemId);
}
// 消费者处理消息
@KafkaListener(topics = "seckill_requests")
public void handleSeckillRequest(String message) {
String[] parts = message.split(":");
orderService.createOrder(parts[0], parts[1]);
}
25~30分钟:练习与拓展
练习题目
- 动态调整限流速率
- 要求:根据系统负载动态修改令牌桶的 rate 参数(如 CPU >80% 时降级为 500 QPS)。
- 库存回滚设计
- 场景:用户超时未支付,将库存返还 Redis 并发送通知。
- 队列优先级
- 任务:实现 VIP 用户的请求优先处理(Redis Sorted Set 或 Kafka 优先级队列)。
推荐拓展方向
- 分布式限流
- 结合 Nginx 层限流(漏桶算法)与 Redis 令牌桶,多层防护。
- 库存分片
- 将单品库存拆分为多个 Redis Key(如 stock:item_1001_shard1),提升并发性能。
- 热点数据隔离
- 对秒杀商品单独部署 Redis 集群,避免影响其他业务。
课程总结
- 流量削峰:令牌桶算法控制入口流量,Lua 脚本保障原子性。
- 库存预热:Redis 缓存 + 原子扣减,避免超卖。
- 请求排队:Redis List 或 Kafka 异步处理,降低数据库压力。
- 关键代码:
- 令牌桶限流 Lua 脚本。
- 库存扣减原子操作。
- 请求队列的生产者-消费者模型。
课后资源
- Redis 官方文档:Redis Transactions
- Kafka 入门指南:Apache Kafka Quickstart
- 完整代码示例:GitHub - 秒杀系统Demo