Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁
Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁
Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁
上集回顾
Redis的分布式锁问题(八)基于Redis的分布式锁_面向鸿蒙编程的博客-CSDN博客https://blog.csdn.net/weixin_43715214/article/details/127967364我们在上一个章节中解决了“分布式锁误删问题”,改进后的代码逻辑如下所示:
但是这仍然不是最佳的实现方案,它在极端的情况下还是会发生问题!
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
如上述代码,如果“判断锁标识”和“释放锁”,之间发生了阻塞呢?(JVM触发FULL GC)
那么之前一节所讲的“锁误删”就有可能发生!!!
所以,我们的解决思路是要保证这两个操作的原子性!
我们可以使用Redis的事务+乐观锁来解决这个问题,但是这样子做非常复杂!这里我们使用 Lua 脚本来实现分布式锁
Lua的简单介绍
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
Lua 教程 | 菜鸟教程 (runoob.com)https://www.runoob.com/lua/lua-tutorial.html
if(0)
then
print("0 为 true")
end
redis调用函数
set name jack
set name Rose,再执行get name
redis的 EVAL 命令
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
在Lua中,数组的下标是从1开始的!!!
Lua脚本解决unLock业务流程
- 获取锁中的线程标示
- 判断是否与指定的标示(当前线程标示)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
代码实现
unLock.lua
-- 获取锁标识,是否与当前线程一致?
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 一致,删除
return redis.call('del', KEYS[1])
end
-- 不一致,直接返回
return 0
RedisTemplate调用Lua脚本的API
Lua 解决unLock问题
/**
* redis的分布式锁
* 实现ILock接口
*/
public class SimpleRedisLock implements ILock {
// 不同的业务有不同的锁名称
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
// DefaultRedisScript,
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
// 初始化 UNLOCK_SCRIPT,用静态代码块的方式,一加载SimpleRedisLock有会加载unlock.lua
// 避免每次调unLock() 才去加载,提升性能!!!
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// setLocation() 设置脚本位置
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 返回值类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 获取锁
*/
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
// set lock thread1 nx ex 10
// nx : setIfAbsent(如果不存在) , ex : timeoutSec(秒)
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 自动拆箱(Boolean -> boolean)!!!可能有风险
return Boolean.TRUE.equals(success);
}
/**
* 解决判断(锁标识、释放锁)这两个动作,之间产生阻塞!!!
* JVM的 FULL GC
* 要让这两个动作具有原子性
*/
@Override
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}