1、目的
使用lua脚本,可以保证多条命令的操作原子性;同时可以减少操作IO(比如说判断redis对应数据是否小于0,小于0就重置为100,这个场景一般是取出来再判断,再存放进行,就至少存在2次IO,用lua脚本一条命令1次IO就解决了,在批量扣减情况存在多次IO,lua脚本1次也可以解决),提高速度,降低IO.
2、使用案列
根据传入的产品标识及数量扣减该产品数量;此处为单个产品扣减,可优化为批量产品传入,lua内部用table处理。
2.1 初始化redis参数
添加产品及库存(hash结构)。添加两种水果的库存。
// 向Redis中添加数据
redisTemplate.opsForHash().put("productMap", "pro1", "{\"name\":\"苹果\",\"stock\":100}");
redisTemplate.opsForHash().put("productMap", "pro2", "{\"name\":\"西瓜\",\"stock\":1200}");
查看结果:
2.2 业务代码
传入lua脚本,实现对应产品库存数量扣减。(可优化为多产品批量扣减)
通过setnx加锁,防止死锁设置锁超时时间,同时业务执行完手动释放锁。设置锁等待时间、及锁等待轮询获取锁。(eg:自旋释放cpu资源重新抢占资源)
@Test
public void tete(){
// 向Redis中添加数据
redisTemplate.opsForHash().put("productMap", "pro1", "{\"name\":\"苹果\",\"stock\":100}");
redisTemplate.opsForHash().put("productMap", "pro2", "{\"name\":\"西瓜\",\"stock\":1200}");
// Lua 脚本字符串
String luaScript = "local productKey = KEYS[1]; " +
"local pro = KEYS[2]; " +
"local lockKey = KEYS[3]; " +
"local lockTimeout = tonumber(ARGV[1]); " +
"local deductAmount = tonumber(ARGV[2]); " +
"local spinIntervalMs = tonumber(ARGV[3]); " +
"local maxSpinCount = tonumber(ARGV[4]); " +
"local lockAcquired = redis.call('setnx', lockKey, 1); " +
"if lockAcquired == 1 then " +
" redis.call('pexpire', lockKey, lockTimeout); " +
" local currentValue = redis.call('hget', productKey, pro); " +
" if currentValue then " +
" local dbObj = cjson.decode(currentValue);" +
" local currentStock = tonumber(dbObj.stock); " +
" if currentStock >= deductAmount then " +
" dbObj.stock = currentStock - deductAmount; " +
" local updatedValue = cjson.encode(dbObj); " +
" redis.call('hset', productKey, pro, updatedValue); " +
" redis.call('del', lockKey); " + // 释放锁
" return true; " +
" else " +
" return false; " +
" end " +
" else " +
" return false; " +
" end " +
"else " +
" local spinCount = 0; " +
" while spinCount < maxSpinCount do " +
" local lockValue = redis.call('get', lockKey); " +
" if not lockValue then " +
" lockAcquired = redis.call('setnx', lockKey, 1); " +
" if lockAcquired == 1 then " +
" redis.call('pexpire', lockKey, lockTimeout); " +
" local currentValue = redis.call('hget', productKey, pro); " +
" if currentValue then " +
" local dbObj = cjson.decode(currentValue);" +
" local currentStock = tonumber(dbObj.stock); " +
" if currentStock >= deductAmount then " +
" dbObj.stock = currentStock - deductAmount; " +
" local updatedValue = cjson.encode(dbObj); " +
" redis.call('hset', productKey, pro, updatedValue); " +
" redis.call('del', lockKey); " +
" return true; " +
" else " +
" return false; " +
" end " +
" else " +
" return false; " +
" end " +
" end " +
" break; " +
" end " +
" spinCount = spinCount + 1; " +
" end " +
" return false; " +
"end";
// 创建DefaultRedisScript对象
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Boolean.class); // 设置返回类型为Boolean
// 执行脚本
Boolean result = (Boolean) redisTemplate.execute(script,
Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数
"50000", // ARGV参数第一个:锁过期时间(毫秒)
"10", // ARGV参数第二个:扣减数量
"1000",// ARGV参数第3个:等待时间
"5");// ARGV参数第4个:轮询次数
System.out.println("result1:"+result);
Boolean result2 = (Boolean) redisTemplate.execute(script,
Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数
"50000", // ARGV参数第一个:锁过期时间(毫秒)
"10", // ARGV参数第二个:扣减数量
"1000",
"5");
System.out.println("result2:"+result2);
Boolean result3 = (Boolean) redisTemplate.execute(script,
Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数
"50000", // ARGV参数第一个:锁过期时间(毫秒)
"10", // ARGV参数第二个:扣减数量
"1000",
"5");
System.out.println("result3:"+result3);
if (result2) {
System.out.println("Stock deduction successful.");
} else {
System.out.println("Insufficient stock or lock already acquired.");
}
// 验证库存是否正确扣减
Object updatedValue = redisTemplate.opsForHash().get("productMap", "pro1");
System.out.println(updatedValue);
Boolean result5 = (Boolean) redisTemplate.execute(script,
Collections.unmodifiableList(List.of("productMap","pro2","lock:pro2")), // KEYS参数
"5000", // ARGV参数第一个:锁过期时间(毫秒)
"500", // ARGV参数第二个:扣减数量
"1000",
"5");
}
2.3 正常执行结果
2.4 若获取锁超时,则会出现扣减失败
脚本执行时间过长会导致。(此处可通过删除手动释放锁实现:模拟业务耗时过长没办法手动释放锁需等待锁国企时间)
第二个扣减等待超时。可通过设置调整添加自旋时间重试或业务代码判断重试机制