redis_点评(16.优惠劵秒杀-Lua 脚本实现秒杀库存 + 一人一单(Redis 异步秒杀)
核心是用 Redis + Lua 脚本 实现「库存校验、一人一单、扣减库存」的原子操作,把并发压力转移到 Redis,实现高并发下的快速秒杀处理。
一、改造整体思路
之前的版本是 Java 代码 + 分布式锁,现在升级为:
-
预热阶段:把库存数据提前加载到 Redis
-
秒杀阶段:通过 Lua 脚本在 Redis 中原子完成校验和扣减
-
异步阶段:把秒杀成功的订单放入阻塞队列,后台异步处理数据库订单
二、核心变更详解
-
预热阶段:把库存数据存入 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);
-
第一行意思:创建一个 “Lua 脚本对象”就像 new 一个工具,用来装我们的 Lua 脚本。
-
第二行意思:告诉程序 → 脚本文件在哪里(seckill.lua 是你写的秒杀 Lua 脚本,放在项目的 resources 目录下)
-
第三行意思:告诉程序 → 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
更多推荐

所有评论(0)