【Redis】利用Java代码调用Lua脚本改造分布式锁

一、API

lua脚本本身并不需要大家花费太多时间去研究,只需要知道如何调用,大致是什么意思即可。

我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图

唯一不同的是Java代码中我们不需要指定key的数量。

image-20240528204056506

二、代码实现

接下来基于这个API改造一下我们的锁。

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

image-20240528204617151

就直接新建一个Lua Script

image-20240528204756613

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

image-20240528204827308

这里脚本就不重新写了,直接复制粘贴之前写的。

脚本中返回值是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,这是个类,而我们写的是一个文件,显然这个类就需要去加载我们写的文件。

image-20240528205211759

那我们是每次释放锁的时候去读取这个文件呢,还是提前将这个文件读取好?显然是提前读取好。如果你每次执行都要去读取文件,就要产生io流,就会导致性能很差,因此建议大家提前将脚本定义好。

ctrl + H 可以发现 RedisScript 是一个接口,它里面有一个实现叫 DefaultRedisScript,因此在这我们会使用它的实现类 DefaultRedisScript,而它的泛型是它的返回值类型。

image-20240528205504700

由于返回值是一个数值,因此直接使用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集群保证高可用和高并发特性

这就是一个相对完善的分布式锁了,相对完善表示它还有进步的空间,那它还有哪些问题可以继续拓展和升级呢?我们下节继续分析。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值