本文主要是介绍redis 结合Lua脚本实现 秒杀、防止超卖,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
需求:
同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 thenreturn -1 -- 用户参加过秒杀了
else-- 获取当前商品库存数量,使用HGET命令从哈希表中获取local goodsCount = redis.call('HGET', stockHashKey, goodsId)if goodsCount == false thengoodsCount = 0 -- 如果没有这个字段,默认库存为0endredis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】当前商品库存量:"..goodsCount)if tonumber(goodsCount) <= 0 then -- 商品抢光了redis.log(redis.LOG_NOTICE,"【"..userId.."-"..goodsId.."】用户秒杀失败。")return 0else -- 还有库存-- 更新库存数量,使用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 1end
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() {//脚本的范型返回值是LongDefaultRedisScript<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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Qualifier(value = "seckill_stockScript")@Autowiredprivate DefaultRedisScript<Long> redisScript;/*** 这里没有写模拟压测的,只写了核心调用逻辑* 可以写个线程池批量压,或是jmeter压*/@Testpublic 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_queuekeys *hget Seckill:Stock goodsId_1
hget Seckill:Stock goodsId_2
hget Seckill:Stock goodsId_3# 查看stream队列中的消息
xrange Seckill:orders_queue - +
这篇关于redis 结合Lua脚本实现 秒杀、防止超卖的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!