核心是用 Redis + Lua 脚本 实现「库存校验、一人一单、扣减库存」的原子操作,把并发压力转移到 Redis,实现高并发下的快速秒杀处理。

一、改造整体思路

之前的版本是 Java 代码 + 分布式锁,现在升级为:

  1. 预热阶段:把库存数据提前加载到 Redis

  2. 秒杀阶段:通过 Lua 脚本在 Redis 中原子完成校验和扣减

  3. 异步阶段:把秒杀成功的订单放入阻塞队列,后台异步处理数据库订单

二、核心变更详解

  1. 预热阶段:把库存数据存入 Redis

// 保存秒杀的库存到Redis中
stringRedisTemplate.opsForValue().set(
    SECKILL_STOCK_KEY + voucher.getId(), 
    voucher.getStock().toString()
);

目的:秒杀前把优惠券库存预加载到 Redis,避免高并发下频繁访问数据库

Key 设计seckill:stock:优惠券ID,直接通过优惠券 ID 就能拿到库存

2、Lua 脚本定义与加载

private static final DefaultRedisScript<Long> SECKILL_SCRIRT;
static {
    SECKILL_SCRIRT = new DefaultRedisScript<>();
    SECKILL_SCRIRT.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_SCRIRT.setResultType(Long.class);
}
  • 提前加载 Lua 脚本,避免每次请求都读取文件

  • 脚本执行后返回一个状态码,用于 Java 层判断结果

1、定义一个静态常量,用来存放 Lua 脚本对象
private static final DefaultRedisScript<Long> SECKILL_SCRIRT;
  • DefaultRedisScript:Spring 提供的封装 Lua 脚本的工具类

  • <Long> :表示 Lua 脚本执行完返回的是数字(0、1、2)

  • static:全局唯一,属于类,启动就加载

  • final:不可修改,是常量

2、static { ... } 静态代码块;

项目启动时执行一次,用来提前加载 Lua 脚本,不是每次调用都加载,性能极高。

3、秒杀 Lua 脚本对象初始化详解
 SECKILL_SCRIRT = new DefaultRedisScript<>();
    SECKILL_SCRIRT.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_SCRIRT.setResultType(Long.class);
  1. 第一行意思:创建一个 “Lua 脚本对象”就像 new 一个工具,用来装我们的 Lua 脚本。

  2. 第二行意思:告诉程序 → 脚本文件在哪里(seckill.lua 是你写的秒杀 Lua 脚本,放在项目的 resources 目录下)

  3. 第三行意思:告诉程序 → Lua 脚本执行完返回什么类型

这里返回类型指定为 Long,因为 DefaultRedisScript 是 Spring 封装的工具,只支持包装类(Long、String、Boolean),不支持基本数据类型(long、int、boolean)

3、业务层秒杀入口改造

@Override
public Result seckillVoucher(Long voucherId) {
    // 获取用户
    Long userId = UserHolder.getUser().getId();
    // 1. 执行Lua脚本
    Long result = stringRedisTemplate.execute(
        SECKILL_SCRIRT,
        Collections.emptyList(), // KEYS参数为空
        voucherId.toString(), userId.toString() // ARGV参数:优惠券ID、用户ID
    );
    int r = result.intValue();
    // 2. 判断结果是否为0
    if(r != 0) {
        // 不为0,代表没有购买资格
        return Result.fail(r == 1 ? "库存不足":"不能重复下单");
    }
    long orderId = redisIdWorker.nextId("order"); //生成全局唯一订单 ID
    // 2.2. 为0,有购买资格,把下单的信息保存到阻塞队列(TODO)
    // 3. 返回订单id(这里先返回0,实际后续异步生成)
    return Result.ok(0);
}
  • 核心变化:秒杀逻辑全部交给 Lua 脚本执行,Java 层只负责调用和处理结果

  • 返回值约定

    • 0:秒杀成功

    • 1:库存不足

    • 2:用户重复下单

  • stringRedisTemplate.execute(脚本, keys参数, args参数);

作用:让 Redis 执行一段 Lua 脚本,并且返回结果。

4、seckill.lua 脚本

-- 参数列表
-- 1.优惠券id
local voucherId = ARGV[1]
-- 2.用户id
local userId = ARGV[2]

-- 数据key
-- 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 脚本业务
-- 1.判断库存是否充足
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 库存不足,返回1
    return 1
end
-- 2.判断用户是否下单
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 存在,重复下单,返回2
    return 2
end
-- 3.扣库存(原子操作)
redis.call('incrby', stockKey, -1)
-- 4.保存用户(一人一单标记)
redis.call('sadd', orderKey, userId)
-- 成功返回0
return 0

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐