需求:
同1商品单个用户限购1件,库存不会超卖
1 Lua脚本,因可实现原子性操作,这个文件放到resources目录下
local userId = KEYS[1] -- 当前秒杀的用户 ID
local goodsId = KEYS[2] -- 秒杀的商品 ID
-- 订单id
local orderId = ARGV[1]
redis.log(redis.LOG_NOTICE,"秒杀商品ID:‘"..goodsId.."’,当前秒杀用户 ID:‘"..userId.."’") -- 日志记录
-- 使用一个统一的前缀来存储所有商品的库存信息
local stockHashKey = "Seckill:Stock" -- 秒杀商品的库存哈希KEY
-- 如果一个用户已经参加过秒杀了,那么不应该重复参加
-- 所有的秒杀的商品一定要保存在 SET 集合(用户 ID 不能重复)
local resultKey = "Seckill:Result:"..goodsId
local resultExists = redis.call('SISMEMBER', resultKey, userId)
redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】当前用户参加秒杀的状态:"..resultExists)
if tonumber(resultExists) == 1 then
return -1 -- 用户参加过秒杀了
else
-- 获取当前商品库存数量,使用HGET命令从哈希表中获取
local goodsCount = redis.call('HGET', stockHashKey, goodsId)
if goodsCount == false then
goodsCount = 0 -- 如果没有这个字段,默认库存为0
end
redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】当前商品库存量:"..goodsCount)
if tonumber(goodsCount) <= 0 then -- 商品抢光了
redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】用户秒杀失败。")
return 0
else -- 还有库存
-- 更新库存数量,使用HINCRBY命令减少库存
redis.call('HINCRBY', stockHashKey, goodsId, -1)
-- 秒杀结果记录
redis.call('SADD', resultKey, userId)
-- 发送一条消息到stream队列中
redis.call('xadd', 'Seckill:orders_queue', '*', 'userId', userId, 'goodsId',goodsId,'orderId', orderId)
redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId .."】用户秒杀成功。")
return 1
end
end
2 写个配置类,读取Lua脚本
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
@Configuration
public class LuaConfiguration {
@Bean(value = "seckill_stockScript")
public DefaultRedisScript<Long> miaosha_stockScript() {
//脚本的范型返回值是Long
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//放在resources目录下
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("sha_2.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
3 主程序逻辑
import lombok.extern.slf4j.Slf4j;
import org.example.service_a.service_a_App;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.List;
@SpringBootTest(classes = {seckillService.class})
@Slf4j
public class Test_2_lua {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Qualifier(value = "seckill_stockScript")
@Autowired
private DefaultRedisScript<Long> redisScript;
/**
* 这里没有写模拟压测的,只写了核心调用逻辑
* 可以写个线程池批量压,或是jmeter压
*/
@Test
public void executeLuaScriptFromFile() {
/**
* 准备Lua脚本所需参数,如 KEYS,ARGV
* 这个脚本 KEYS 为 用户id,商品id
* ARGV 为 订单id
* 这些通常是键名,用于在脚本中作为变量使用
*/
List<String> keys = Arrays.asList("user_1", "goodsId_1");
// hutool 工具类雪花算法,默认调用的是getSnowflake()方法,生成id
// createSnowflake(long workerId, long datacenterId) 方法,是每次调用都会创建一个新的Snowflake对象,不同的Snowflake对象创建的ID可能会有重复,不推荐
String orderId = IdUtil.getSnowflakeNextIdStr();
//脚本里返回1说明秒杀成功
Long execute = stringRedisTemplate.execute(redisScript, keys,orderId );
}
}
4 redis准备测试数据,这里模拟了些数据
#用hash结构存储商品id和库存数量,默认10个库存
hset Seckill:Stock goodsId_1 10
hset Seckill:Stock goodsId_2 10
hset Seckill:Stock goodsId_3 10
#用set结构存储某个商品id中,秒杀成功的用户id列表
del Seckill:Result:goodsId_1
del Seckill:Result:goodsId_2
del Seckill:Result:goodsId_3
# stream类型的队列,有产品id,用户id,订单id
del Seckill:orders_queue
keys *
hget Seckill:Stock goodsId_1
hget Seckill:Stock goodsId_2
hget Seckill:Stock goodsId_3
# 查看stream队列中的消息
xrange Seckill:orders_queue - +