基于Lua+Redis实现库存的秒杀扣减
在库存扣减中,我们一般使用lua脚本来保证原子性
以下是库存和库存流水记录的lua脚本:
KEYS[1] = 库存的 key
KEYS[2] = 流水的 key
ARGV[1] = 本次要扣减的库存数
AGRV[2] = 本次扣减的唯一编号
String luaScript = """
-- 检查操作是否已经执行过,通过检查哈希表(KEYS[2])中是否存在标识(ARGV[2])
if redis.call('hexists', KEYS[2], ARGV[2]) == 1 then
-- 如果存在,返回错误信息'OPERATION_ALREADY_EXECUTED'
return redis.error_reply('OPERATION_ALREADY_EXECUTED')
end
-- 获取当前库存值
local current = redis.call('get', KEYS[1])
-- 如果库存键不存在,返回错误信息'KEY_NOT_FOUND'
if current == false then
return redis.error_reply('KEY_NOT_FOUND')
end
-- 如果当前值不是数字,返回错误信息'current value is not a number'
if tonumber(current) == nil then
return redis.error_reply('current value is not a number')
end
-- 如果当前库存小于请求的扣减数量,返回错误信息'INVENTORY_NOT_ENOUGH'
if tonumber(current) < tonumber(ARGV[1]) then
return redis.error_reply('INVENTORY_NOT_ENOUGH')
end
-- 计算新的库存值
local new = tonumber(current) - tonumber(ARGV[1])
-- 设置新的库存值
redis.call('set', KEYS[1], tostring(new))
-- 获取Redis服务器的当前时间(秒和微秒)
local time = redis.call("time")
-- 转换为毫秒级时间戳
local currentTimeMillis = (time[1] * 1000) + math.floor(time[2] / 1000)
-- 使用哈希结构存储操作日志
redis.call('hset', KEYS[2], ARGV[2], cjson.encode({
action = "decrease", -- 操作类型:增加
from = current, -- 操作前的库存值
to = new, -- 操作后的库存值
change = ARGV[1], -- 变化量
by = ARGV[2], -- 操作标识
timestamp = currentTimeMillis -- 操作时间戳
}))
-- 返回新的库存值
return new
""";
JAVA调用
/**
* Executes Lua script
*
* @param <R> - type of result
* @param mode - execution mode READ OR WRITE
* @param luaScript - lua script lua脚本
* @param returnType - return type 返回类型
* @param keys - keys available through KEYS param in script key集合
* @param values - values available through VALUES param in script values参数
* @return result object
*/
<R> R eval(Mode mode, String luaScript, ReturnType returnType, List<Object> keys, Object... values);
@Autowired
private RedissonClient redissonClient;
Integer result = ((Long) redissonClient.getScript().eval(RScript.Mode.READ_WRITE,
luaScript,
RScript.ReturnType.INTEGER,
Arrays.asList(KEYS[1], KEYS[2]),//KEYS[1], KEYS[2]看具体业务字段
ARGV[1],ARGV[2])).intValue();//ARGV[1],ARGV[2]看具体业务字段