一、API
lua脚本本身并不需要大家花费太多时间去研究,只需要知道如何调用,大致是什么意思即可。
我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图
唯一不同的是Java代码中我们不需要指定key的数量。

二、代码实现
接下来基于这个API改造一下我们的锁。
脚本不建议大家在代码中直接写死,因为你写死在这里的话,奖励啊如果我们需要对这个脚本做一些调整的时候就非常不方便了,所以我们会将脚本写到一个文件中,例如这里安装 EmmyLua插件

就直接新建一个Lua Script

起名叫 unlock
,即释放锁的意思

这里脚本就不重新写了,直接复制粘贴之前写的。
脚本中返回值是0或1,是数值类型
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1]) -- 1代表成功
end
-- 不一致,则直接返回0,0代表失败
return 0
接下来就是到Java中调用了
SimpleRedisLock.java
excute函数
中接收三个参数,分别是脚本、key、args。脚本的类型叫 RedisScript
,这是个类,而我们写的是一个文件,显然这个类就需要去加载我们写的文件。
那我们是每次释放锁的时候去读取这个文件呢,还是提前将这个文件读取好?显然是提前读取好。如果你每次执行都要去读取文件,就要产生io流,就会导致性能很差,因此建议大家提前将脚本定义好。
ctrl + H 可以发现 RedisScript
是一个接口,它里面有一个实现叫 DefaultRedisScript
,因此在这我们会使用它的实现类 DefaultRedisScript
,而它的泛型是它的返回值类型。
由于返回值是一个数值,因此直接使用long。但如果你不关心返回值,那直接返回Object也是可以的。
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 在静态代码块中初始化
static {
// DefaultRedisScript构造函数中可以接收字符串类型的脚本,也就是将脚本当成字符串直接传进来,这样就相当于硬编码了,我们还是不建议这么做,我们还是放文件中,将来去修改比较方便。
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 这里调用它的set方法去指定脚本的位置
// ctrl + p查看参数,可以看见它里面需要传入Resource,即资源,这里我们可以使用spring中提供的ClassPathResource,也就是ClassPath下的一些资源,它默认就回去ClassPath下找,而我们是放在resouces目录下的,resource就是ClassPath,因此我们可以直接指定文件名
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
// 配置返回值类型
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name), // 指定锁的key,存进去的是应该集合。singletonList:单元素集合的意思
ID_PREFIX + Thread.currentThread().getId()); // 线程的标识
}
此时我们释放锁的代码已经变成一行代码了,之前出现线程安全问题就是因为它是两行代码,先查询,然后判断,最后释放,如果在判断完、释放前出现了一个阻塞,然后导致超时释放,就会出现误删的情况了。
现在它变成一行代码,更重要的是这一行代码调用的是Lua脚本,判断、删除是在脚本中执行的,是能够满足原子性的。
测试逻辑:
第一个线程进来,得到了锁,手动删除锁,模拟锁超时了,其他线程会执行lua来抢锁,当第一天线程利用lua删除锁时,lua能保证他不能删除他的锁,第二个线程删除锁时,利用lua同样可以保证不会删除别人的锁,同时还能保证原子性。
三、总结
现在我们实现的分布式锁其实就已经是一个生产可用的、相对完善的分布式锁了。
小总结:
基于Redis的分布式锁实现思路:
-
获取锁的时候利用set nx ex获取锁,
set nx
目的是互斥,确保只有一个线程能拿到锁,ex
是一个兜底方案,防止宕机导致锁无法释放。 -
释放锁时先判断线程标示是否与自己一致,一致则删除锁,防止误删
并且使用Lua脚本保证它的原子性,这样避免在多线程的情况下因为阻塞导致的误删。
那这样的分布式锁有什么特性呢?
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Lua脚本确保原子性,避免误删的问题
- 利用Redis集群保证高可用和高并发特性
这就是一个相对完善的分布式锁了,相对完善表示它还有进步的空间,那它还有哪些问题可以继续拓展和升级呢?我们下节继续分析。