基于Redis Lua脚本的秒杀系统

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:123Set{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. 总结

这个秒杀方案的核心优势

  1. 原子性:Lua脚本保证所有操作的原子性

  2. 高性能:内存操作,响应时间在毫秒级别

  3. 可扩展:支持高并发,通过Stream实现异步处理

  4. 数据一致:Redis与数据库最终一致性

  5. 防止超卖:库存检查与扣减原子完成

  6. 一人一单:Set数据结构天然去重

这种设计能够支撑万级QPS的秒杀场景,是电商高并发场景的经典解决方案。

相关推荐
0和1的舞者2 小时前
《网络编程核心概念与 UDP Socket 组件深度解析》
java·开发语言·网络·计算机网络·udp·socket
稚辉君.MCA_P8_Java2 小时前
Gemini永久会员 Java动态规划
java·数据结构·leetcode·排序算法·动态规划
oioihoii2 小时前
C++语言演进之路:从“C with Classes”到现代编程基石
java·c语言·c++
N***73852 小时前
SQL锁机制
java·数据库·sql
Java天梯之路2 小时前
Java 初学者必看:接口 vs 抽象类,到底有什么区别?
java·开发语言
小熊officer2 小时前
Nginx中正向代理,反向代理,负载均衡
java·nginx·负载均衡
信码由缰2 小时前
Java 应用容器化与部署
java
三翼鸟数字化技术团队2 小时前
基于redis的多资源分布式公平锁的设计与实践
redis·后端
方白羽3 小时前
Kotlin遇上Java 静态方法
android·java·kotlin