LUA脚本改造redis分布式锁
创始人
2024-11-11 19:39:07
0

在redis集群模式下,我们会启动多个tomcat实例,每个tomcat实例都有一个JVM,且不共享。而synchronize锁的作用范围仅仅是当前JVM,所以我们需要一个作用于集群下的锁,也就是分布式锁。(就是不能用JVM自带的锁了,需要一个第三方应用实现锁)

在redis集群中我们是用setnx实现互斥锁来实现分布式锁。

setnx key value NX EX  时间 是往redis中创建一个key-value键值对,如果redis中没有该key,则创建成功,返回true;如果redis已经有了该key,则创建失败,返回null。NX是互斥,EX是超时时间,后跟具体时间+单位(s),EX用于过期时间,过了过期时间自动删除该key-value键值对。

我们用setnx命令实现分布式锁时,key和value都是String类型,key一般是特定前缀+该锁的名字,

value为线程id。

为什么value要存线程id呢?

因为存在这样情况:线程一获取锁,去执行逻辑,过程中遇到网络动荡,线程一卡住了,然后一段时间后,线程一获取的锁的过期时间到了,线程一的锁自动释放。然后线程二来获取锁,线程二获取成功,去执行逻辑,而在这个过程中线程一的网络动荡恢复了,线程一继续执行,线程一先于线程二执行完,线程一去释放锁,但此时线程一创建的锁已经因超时而自动释放了,所以此时线程一会去错误的释放线程二的锁,所以我们要把线程id存入加以判断,防止误删其他线程的id。

代码实现获取锁和释放锁:

package com.hmdp.utils;  import cn.hutool.core.lang.UUID; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript;  import java.util.Collections; import java.util.concurrent.TimeUnit;  public class SimpleRedisLock implements ILock {      private String name;     private StringRedisTemplate stringRedisTemplate;      public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {         this.name = name;         this.stringRedisTemplate = stringRedisTemplate;     }      private static final String KEY_PREFIX = "lock:";      //randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线     //因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM     private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";      @Override     public boolean tryLock(long timeoutSec) {         // 获取线程标示         String threadId = ID_PREFIX + Thread.currentThread().getId();         // 获取锁         Boolean success = stringRedisTemplate.opsForValue()                 .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);         //不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据         return Boolean.TRUE.equals(success);     }      @Override     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);         }     } } 

我们需要新建一个类实现ILock接口,然后去重写其中的tryLock获取锁、unlock释放锁方法。

往SimpleRedisLock的构造方法传入name变量,方便我们在实例化SimpleRedisLock类中设置该锁的名字。我们后面用setnx命令创建锁时的key值就是特定前缀KEY_PREFIX加上这个name;value值是randomUUID生成的唯一一串数字加上该线程id,因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM。

问题:

当本线程1在进入这个if判断后(释放锁之前),突然阻塞(比如full GC,该JVM上全服务堵塞)阻塞时间过长,锁超时释放,这时另一个jvm的线程2获取到锁,然后线程1继续执行释放锁
这样就又会出现同一个用户会有俩个线程在同时运行,所以需要保证判断和释放锁这俩步为一个原子性,同成功或同失败
这样很容易想到事务,redis也有事务,但redis的事务可以保证原子性,但不能保证一致性,而且redis的事务,是最后事务中的步骤同时完成。并不是一步一步的执行,所以只能用乐观锁但不建议用乐观锁,推荐用lua脚本

一个lua脚本中可以编写多条redis命令,确保多条命令的原子性。即实现判断和释放锁一起执行的原子性。

我们需要把这个脚本写在resources目录下

释放锁的lua脚本:

local key=KEYS[1]  -- 锁的key local threadId=ARGV[1]  --线程唯一标识 -- 比较线程标示与锁中的标示是否一致 if(redis.call('get', key) ==  threadId) then     -- 释放锁 del key     return redis.call('del', key) end return 0;

if()  then  end 相当于Java命令中的  if(  ) {  }   。

KEYS[1]:为需要传入的key值,当需要传入多个key值时,声明KEYS[2]、KEYS[3]...等就行。

ARGV[1]:为需要传入的其他非key的变量,声明方法与key一样。例如:

local key= KEYS[1]  local key2= KEYS[2] local threadId=ARGV[1] local releaseTime=ARGV[2]

 

redis.call('  ',  ....)是执行的redis命令,命令中单引号'   '中写要执行的redis操作,后面的参数为执行该redis命令所需的参数,例如,get命令,需要知道key值。

然后改写我们的分布式锁:

我们需要先加载我们的lua脚本:

private static final DefaultRedisScript UNLOCK_SCRIPT;     static {         UNLOCK_SCRIPT = new DefaultRedisScript<>();         UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); //加载的脚本的位置,如果脚本文件在resources目录下,则只需写脚本名称即可。         UNLOCK_SCRIPT.setResultType(Long.class); //返回值的类型     } @Override     public void unlock() {         // 调用lua脚本  ,原来的多行代码变成了现在的单行代码就保证了原子性         stringRedisTemplate.execute(                 UNLOCK_SCRIPT,                 Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数                 ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数     }

完整代码:

package com.hmdp.utils;  import cn.hutool.core.lang.UUID; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript;  import java.util.Collections; import java.util.concurrent.TimeUnit;  public class SimpleRedisLock implements ILock {      private String name;     private StringRedisTemplate stringRedisTemplate;      public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {         this.name = name;         this.stringRedisTemplate = stringRedisTemplate;     }      private static final String KEY_PREFIX = "lock:";      //randomUUID生成的数字会带一个横线,toString(true)方法就是去掉横线     //因为不同JVM中,可能存在线程号相同的情况,所以需要用UUID来区分不同的JVM     private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";      private static final DefaultRedisScript UNLOCK_SCRIPT;     static {         UNLOCK_SCRIPT = new DefaultRedisScript<>();         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();         // 获取锁         Boolean success = stringRedisTemplate.opsForValue()                 .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);         //不能直接返回success,因为会有自动拆箱的风险,如果success是null,就会返回true,返回错误数据         return Boolean.TRUE.equals(success);     }      @Override     public void unlock() {         // 调用lua脚本  ,原来的多行代码变成了现在的单行代码就保证了原子性         stringRedisTemplate.execute(                 UNLOCK_SCRIPT,                 Collections.singletonList(KEY_PREFIX + name), //生成单元素的集合,即脚本中的需要的KETS[1]参数                 ID_PREFIX + Thread.currentThread().getId()); //即脚本中需要的ARVG[1]参数     }  } 

相关内容

热门资讯

有玩家发现!凑一桌辅助器怎么安... 有玩家发现!凑一桌辅助器怎么安装,微乐兰州麻将小程序辅助(本来是真的挂)-哔哩哔哩1、微乐兰州麻将小...
这一现象值得深思!微信小程序中... 这一现象值得深思!微信小程序中至上饶510k辅助器,点我休闲辅助器(其实是有脚本)-哔哩哔哩微信小程...
经调查!花花生活圈辅助,功夫川... 经调查!花花生活圈辅助,功夫川麻老是输什么情况(切实真的是有下载)-哔哩哔哩功夫川麻老是输什么情况辅...
日前!亲友圈辅助吧,牵手app... 日前!亲友圈辅助吧,牵手app破解(本来真的有下载)-哔哩哔哩1、下载好亲友圈辅助吧脚本下载之后点击...
反观!开心庄园脚本辅助器,长城... 反观!开心庄园脚本辅助器,长城互娱辅助(竟然是有app)-哔哩哔哩1、玩家可以在开心庄园脚本辅助器透...
更值得关注的是!大巴杭州辅助,... 更值得关注的是!大巴杭州辅助,纳祥游戏科技(确实真的有挂)-哔哩哔哩该软件可以轻松地帮助玩家将纳祥游...
今天上午!同乡游辅助软件下载,... 今天上午!同乡游辅助软件下载,微信小程序免费黑科技(确实真的是有app)-哔哩哔哩微信小程序免费黑科...
据目击者称!江湖悠悠手游多开辅... 据目击者称!江湖悠悠手游多开辅助,美猴王大厅怎么修改数据(真是是真的插件)-哔哩哔哩1、每一步都需要...
日前!都莱大菠萝辅助,川娱竞技... 日前!都莱大菠萝辅助,川娱竞技插件(切实是有脚本)-哔哩哔哩1、很好的工具软件,可以解锁游戏的都莱大...
长期以来!菜鸟黑桃a3作z弊,... 您好,江西中至小程序黑科技这款游戏可以开挂的,确实是有挂的,需要了解加去威信【136704302】很...