1. 整体架构设计
1.1 秒杀流程
用户请求 → Lua脚本原子操作 → 返回结果 → 异步处理订单
1.2 核心思想
java
-- 1.参数列表
local voucherId = ARGV[1] -- 优惠券ID
local userId = ARGV[2] -- 用户ID
local orderId = ARGV[3] -- 预生成的订单ID
-
原子性:使用Lua脚本保证所有操作在Redis中原子执行
-
高性能:库存判断和扣减在内存中完成
-
异步化:订单创建通过消息队列异步处理
2. Lua脚本详细解析
2.1 参数定义
java
-- 1.参数列表
local voucherId = ARGV[1] -- 优惠券ID
local userId = ARGV[2] -- 用户ID
local orderId = ARGV[3] -- 预生成的订单ID
2.2 Key定义
java
-- 2.数据key
local stockKey = 'seckill:stock:' .. voucherId -- 库存Key
local orderKey = 'seckill:order:' .. voucherId -- 已下单用户集合Key
Redis数据结构:
-
seckill:stock:123→"100"(String类型,存储库存数量) -
seckill:order:123→Set{userId1, userId2, ...}(Set类型,存储已下单用户ID)
2.3 业务逻辑核心
2.3.1 库存检查
java
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
return 1 -- 库存不足
end
2.3.2 重复下单检查
java
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
return 2 -- 重复下单
end
2.3.3 扣减库存和记录用户
java
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
2.3.4 发送消息到Stream
java
-- 3.6.发送消息到队列中
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
Stream消息内容:
bash
{
"userId": 12345,
"voucherId": 1001,
"id": 67890
}
3. Java代码执行流程
3.1 方法入口
java
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户ID
Long userId = UserHolder.getUser().getId();
// 预生成订单ID(雪花算法)
long orderId = redisIdWorker.nextId("order");
// 执行Lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT, // Lua脚本
Collections.emptyList(), // Keys列表(空)
voucherId.toString(), userId.toString(), String.valueOf(orderId) // 参数
);
3.2 结果处理
java
int r = result.intValue();
// 2.判断结果是否为0
if (r != 0) {
// 2.1.不为0 ,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 3.返回订单id
return Result.ok(orderId);
4. 为什么使用Lua脚本?
4.1 原子性保证
java
// 如果不是Lua脚本,需要多个Redis命令:
// 问题:非原子性,可能产生竞态条件
Long stock = stringRedisTemplate.opsForValue().decrement(stockKey);
if (stock < 0) {
// 库存不足,需要回滚
stringRedisTemplate.opsForValue().increment(stockKey);
return Result.fail("库存不足");
}
// 检查是否重复下单
Boolean isMember = stringRedisTemplate.opsForSet().isMember(orderKey, userId.toString());
if (Boolean.TRUE.equals(isMember)) {
// 重复下单,需要回滚库存
stringRedisTemplate.opsForValue().increment(stockKey);
return Result.fail("不能重复下单");
}
// 记录用户
stringRedisTemplate.opsForSet().add(orderKey, userId.toString());
4.2 性能优势
-
减少网络开销:多个操作一次执行
-
避免竞态条件:所有操作在Redis服务器端原子执行
-
简化错误处理:不需要复杂的回滚逻辑
5. Redis Stream消息队列
5.1 Stream数据结构
java
stream.orders:
{
"1640995200000-0": {
"userId": "12345",
"voucherId": "1001",
"id": "67890"
}
}
5.2 消费者处理
java
@Component
public class OrderStreamConsumer {
@Autowired
private IVoucherOrderService orderService;
@PostConstruct
public void consumeOrders() {
while (true) {
// 从stream.orders读取消息
List<MapRecord<String, Object, Object>> list =
stringRedisTemplate.opsForStream().read(
Consumer.from("group1", "consumer1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
if (list != null && !list.isEmpty()) {
// 处理订单创建
for (MapRecord<String, Object, Object> record : list) {
Map<Object, Object> values = record.getValue();
Long userId = Long.valueOf((String) values.get("userId"));
Long voucherId = Long.valueOf((String) values.get("voucherId"));
Long orderId = Long.valueOf((String) values.get("id"));
// 创建订单(数据库操作)
orderService.createVoucherOrder(voucherId, userId, orderId);
// 确认消息
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "group1", record.getId());
}
}
}
}
}
6. 完整的秒杀系统架构
6.1 数据流
bash
用户请求 → Lua脚本(原子操作) → Stream消息 → 异步消费者 → 数据库
↓
立即响应
6.2 各组件职责
-
Lua脚本:库存扣减、重复校验、消息发送
-
Redis:库存管理、用户去重、消息队列
-
Java服务:脚本执行、结果返回
-
异步消费者:订单持久化
7. 异常情况和处理
7.1 Lua脚本执行失败
-
Redis服务器异常
-
脚本语法错误
-
网络问题
处理:直接返回错误,库存和用户记录不变
7.2 消息处理失败
java
// 消费者需要处理异常
try {
orderService.createVoucherOrder(voucherId, userId, orderId);
// 确认消息
stringRedisTemplate.opsForStream().acknowledge("stream.orders", "group1", record.getId());
} catch (Exception e) {
// 记录日志,消息会重新投递
log.error("创建订单失败: {}", e.getMessage());
}
8. 性能优化点
8.1 预生成订单ID
java
long orderId = redisIdWorker.nextId("order");
-
提前生成,减少关键路径上的耗时
-
使用雪花算法,保证分布式唯一性
8.2 空List参数
java
Collections.emptyList() // 没有KEYS参数,只有ARGV
- 避免Key slot限制,支持集群模式
9. 总结
这个秒杀方案的核心优势:
-
原子性:Lua脚本保证所有操作的原子性
-
高性能:内存操作,响应时间在毫秒级别
-
可扩展:支持高并发,通过Stream实现异步处理
-
数据一致:Redis与数据库最终一致性
-
防止超卖:库存检查与扣减原子完成
-
一人一单:Set数据结构天然去重
这种设计能够支撑万级QPS的秒杀场景,是电商高并发场景的经典解决方案。