分布式锁(redisson,看门狗,主从一致性)

news/2024/11/2 19:21:18/

目录

  • 分布式
    • 一:基本原理和实现方式
    • 二:分布式锁的实现
      • 1:分布式锁的误删问题
      • 2:解决误删问题
    • 三:lua脚本解决多条命令原子性问题
      • 调用lua脚本
    • 四:Redisson
      • 1:redisson入门
      • 2:redisson可重入锁的原理
      • 3:解决尝试等待,超时释放的问题(看门狗)
      • 4:主从一致性问题

分布式

一:基本原理和实现方式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用redis实现分布式锁:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们实现分布式锁有两个基本方法:

一个是获取锁

一个是删除锁

获取我们使用setnx,只有不存在才会set成功作为互斥锁;

删除直接删除这个锁的键就行了;

但是要考虑一种情况,就是获取锁之后服务宕机了,那么就无法释放锁,也就会出现死锁,服务挂了,为了预防这种情况的发生我们要设置过期时间,就算服务宕机,过了过期时间锁也会释放;

还有一种情况就是设置锁,还没去设置锁的过期时间这个时候服务就宕机了,那么也会出现死锁,我们要保证获取锁和设置过期时间这个操作的原子性,redis种就提供了方法可以同时设置过期时间和互斥锁:

set lock t1 nx ex 10

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二:分布式锁的实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们去实现这个接口:

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_VALUE ="lock:";/*** 获取锁* @param timeoutSec* @return*/@Overridepublic boolean trylock(long timeoutSec) {//获取当前线程id作为锁的valuelong id = Thread.currentThread().getId();//设值尝试获取锁,true是成功,false是失败Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_VALUE + name, id + "", timeoutSec, TimeUnit.SECONDS);//因为直接返回b的话jvm会自动装箱拆箱,可能会造成空指针异常,如果b为true返回就是true,如果b为false返回就是false,如果为空返回也是false;return Boolean.TRUE.equals(b);}/*** 释放锁*/@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_VALUE + name);}
}

我们在一人一单的问题使用分布式锁:

Long id = UserHolder.getUser().getId();
SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + id, stringRedisTemplate);
boolean trylock = simpleRedisLock.trylock(1200);
if (!trylock){return Result.fail("一人只能下一单");
}
try {IVoucherOrderService orderService = (IVoucherOrderService) AopContext.currentProxy();return orderService.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {throw new RuntimeException(e);
}finally {simpleRedisLock.unlock();
}

1:分布式锁的误删问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

存在的问题是这样的:

线程1去获取锁,但是呢,业务阻塞超时了,锁就自动释放了,这个时候线程2来了,因为锁释放了,线程2可以获取到锁,就在这时,线程1阻塞通过了,完成了业务之后就要释放锁了,这个时候释放的锁是线程2的锁,线程2还在执行,这个时候线程3也去获取锁成功了,那么就出现了线程2,3并发执行的情况,造成并发安全问题;

这里的问题是线程释放锁释放的不是自己的锁,所以我们解决办法就是在线程释放的时候判断一下是不是自己的锁,怎么判断呢,我们之前获取锁的时候存入的value是线程的id,这个就是线程的唯一标识,我们只需要判断当前线程id和存入的value是否一致就行;

2:解决误删问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//锁的前缀是个常量
private static final String KEY_VALUE ="lock:";
//加上线程的前缀,保证不同集群不同线程的id是唯一
public static final String ID_VALUE= UUID.randomUUID().toString(true);/*** 获取锁* @param timeoutSec* @return*/
@Override
public boolean trylock(long timeoutSec) {//获取当前线程id作为锁的valueString id =ID_VALUE+ Thread.currentThread().getId();//设值尝试获取锁,true是成功,false是失败Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(KEY_VALUE + name, id + "", timeoutSec, TimeUnit.SECONDS);//因为直接返回b的话jvm会自动装箱拆箱,可能会造成空指针异常,如果b为true返回就是true,如果b为false返回就是false,如果为空返回也是false;return Boolean.TRUE.equals(b);
}

只要在锁获取的时候给线程加上唯一标识就行,我们原来用的是线程id,这样不能保证唯一,因为线程id是在同一个jvm内部是递增的,不同jvm的线程id可能相同,那么就需要加上前缀保证线程标识的唯一性,那么就可以使用uuid来保证每一台jvm的uuid是不同的,标识就是唯一的;

释放锁:

@Override
public void unlock() {if (stringRedisTemplate.opsForValue().get(KEY_VALUE + name).equals(ID_VALUE+ Thread.currentThread().getId())){stringRedisTemplate.delete(KEY_VALUE + name);}
}

判断锁中的val和当前线程的标识是否一致;

这样就解决了误删的问题;

但是还有一个问题:

假设一个线程1获取锁之后执行业务,执行完之后,要释放锁,判断了当前锁是否是自己的(锁的value),在要执行删除操作的时候堵塞了,锁超时释放了,这个时候线程2获取锁,执行业务,就在这时,线程1阻塞结束,执行的代码在释放锁上,因为同一个业务锁的key都是一样的,所以线程一能够把锁释放,这时线程2还没执行结束,线程3来了,就出现了线程安全问题;

所以说我们要将判断锁和释放锁作为一个原子性事件,要么同时发生要么同时失败

这就要用lua了

三:lua脚本解决多条命令原子性问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

--获取当前key
local key =KEY[1]
--获取当前线程标识
local id =ARGV[1]--获取锁中的标识
local _id=redis.call('get',key)
--判断是否是同一个标识:
if(id==_id) thenreturn redis.call("del",key)
end
return 0

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在idea中我们执行lua

脚本我们可以在resouce中创建一个lua文件:

要安装插件才行:

插件名叫emmylua

调用lua脚本

//声明一个DefaultRedisScript,原来调取lua脚本
public static final DefaultRedisScript UNLOCK_LUA;
static {//初始化UNLOCK_LUA=new DefaultRedisScript<>();//定位到lua脚本的位置UNLOCK_LUA.setLocation(new ClassPathResource("unlock.lua"));//设置lua脚本的返回值UNLOCK_LUA.setResultType(Long.class);
}
@Override
public void unlock() {// Collections.singletonList创建单元素的集合stringRedisTemplate.execute(UNLOCK_LUA,Collections.singletonList(KEY_VALUE + name),(ID_VALUE+ Thread.currentThread().getId()));
}

这样将判断标识和释放锁的命令写在lua脚本中,就能够保证命令执行的原子性,就不会出现之前说的误删的情况

我们实现分布式锁的思路:

获取锁:使用setnx,互斥的特性,获取值相当于设置锁,使用uuid保证线程的唯一性;

释放锁:释放之前判断锁释放是当前线程的锁,并且将判断锁和释放锁的命令写在一个lua脚本中保证执行命令的原子性;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

四:Redisson

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redisson_254">1:redisson入门

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

步骤1:

引入依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.33.0</version>
</dependency>

步骤二:

配置客户端:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){Config config = new Config();config.useSingleServer().setAddress("redis://123.249.86.145:6379").setPassword("12345");return Redisson.create(config);}
}

redisson_290">2:redisson可重入锁的原理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可重入锁的逻辑:

获取锁:如果锁不存在就获取锁,然后设置过期时间,如果存在了,就判断锁的线程标识是否是自己,如果是自己,那么就让锁的value加一;

释放锁:释放锁的时候判断锁是否是自己的,如果不是自己的就返回空,如果是自己的就让value-1,然后再判断value是否大于0,如果大于0,就重置锁的时间,等于0就删除锁

3:解决尝试等待,超时释放的问题(看门狗)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

redisson内部是如何解决重复尝试的呢?尝试解读一下:首先线程会尝试获取锁,获取锁成功就返回空,获取锁失败就返回ttl,也就是锁的超时释放时间,我们重复尝试肯定是建立在获取锁失败的基础上,当返回的是ttl时,我们会判断当前时间减去我们尝试获取锁的时刻的时间,也就是尝试获取锁使用的时间,是否大于我们设置的等待时间,如果大于我们设置的等待时间,那么就尝试获取锁失败,就返回false,如果还有时间,我们不是直接再去获取锁,而是通过订阅机制,我们等待释放锁的信号,如果等待时间超时就直接返回false,如果没抄时,我们在重新尝试锁,失败在判断是否还有时间然后定义然后重试,就这么循环下去,要么拿到锁,要么超过等待时间获取锁失败;这样就解决我们获取锁失败一次就返回失败,可以一直尝试;

然后再来讲一下超时释放问题,也就是过了设置的超时时间,业务还没执行完,锁就释放了,出现了并发安全问题,怎么解决的呢,我们如果没有设置这个锁的超时释放时间,那么redisson就会自动给我们的超时释放时间设置为-1,这个时候才会开启看门狗机制,具体是什么情况呢,就是内部会开启一个任务,这个任务会一直刷新锁的超时时间,一直无限的刷新,每10秒刷新一次,刷新一次延长30秒,也就会一直存在这个锁,知道完成任务释放锁才会取消这个看门狗任务;

那么释放锁的时候如果释放失败就返回异常,释放成功就要做两件事,一个是发送释放锁的信息给正在订阅尝试获取锁的,还有一件事就是将看门狗任务终止;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4:主从一致性问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

主从一致性问题:在redis集群中有主从关系,一个主节点有多个从节点,当主节点宕机时,会选一个从节点作为主节点;

主节点和从节点为了数据一样会做主从同步操作;有一个问题就是当java应用向主节点发送请求获取锁,获取锁成功了,但是在主从同步还没完成的时候,主节点宕机了,这个时候哨兵就会发现宕机,然后从从节点中选出一个作为主节点,而因为数据未同步,新的主节点中没有锁,也就是锁失效了,那么其他线程发送请求就能获取锁,这就是锁失效的问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如何解决呢:我们不设置主从关系,只设置节点,每个节点都相当与主节点,而且获取锁的时候,必须要所有的节点都获取锁成功才算获取锁成功,如果有一个节点宕机了,他的从节点成为了新的节点,这个时候有线程来获取锁也是不成功的,因为要在所有节点中获取锁,而其他节点已经持有锁了;这就解决了主从一致性问题;

,获取锁成功了,但是在主从同步还没完成的时候,主节点宕机了,这个时候哨兵就会发现宕机,然后从从节点中选出一个作为主节点,而因为数据未同步,新的主节点中没有锁,也就是锁失效了,那么其他线程发送请求就能获取锁,这就是锁失效的问题

[外链图片转存中…(img-1BgOGPaG-1730426881642)]

如何解决呢:我们不设置主从关系,只设置节点,每个节点都相当与主节点,而且获取锁的时候,必须要所有的节点都获取锁成功才算获取锁成功,如果有一个节点宕机了,他的从节点成为了新的节点,这个时候有线程来获取锁也是不成功的,因为要在所有节点中获取锁,而其他节点已经持有锁了;这就解决了主从一致性问题;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


http://www.ppmy.cn/news/1543948.html

相关文章

.bixi勒索病毒来袭:如何防止文件加密与数据丢失?

导言 在网络威胁剧烈的今天&#xff0c;勒索病毒已成为企业和个人面临的重大安全挑战&#xff0c;其中虫洞勒索病毒习得高强度的加密手段和急剧传播的特性引起关注。一旦感染&#xff0c;就会加密关键数据并索要赎金&#xff0c;导致数据无法访问并带来巨大的财务损失。更为严…

春之竞赛:Spring Boot大学生竞赛管理平台

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理大学生竞赛管理系统的相关信息成为必然。开…

openEuler下配置openGauss环境图解

一、在openEuler中创建用户&#xff0c;并授予权限 # 创建用户 sudo adduser omm# 授予权限 chown omm /opt# 切换用户 su - omm 二、在openGauss官网找到openGauss极简版的软件包 openGauss软件 | openGauss下载 | openGauss软件包 | openGauss社区 右键立即下载&#xff0…

VSCode中安装RN相关的插件(一)

一、插件介绍 ES7 React / Redux / React-Native snippets v4.4.3 如图&#xff0c;在VSCode的插件商店里搜索该插件进行安装。 安装完插件以后&#xff0c;就可以通过简写指令生成一些代码片段了。具体有哪些简短指令&#xff0c;大家可以自己查看&#xff0c;这里举两个示例…

独立北斗定位智能安全帽、定位安全帽、单北斗执法记录仪

AIoT万物智联&#xff0c;智能安全帽生产厂家&#xff0c;执法记录仪生产厂家&#xff0c;单北斗定位智能安全帽、智能头盔、头盔记录仪、执法记录仪、智能视频分析/边缘计算AI盒子、车载视频监控/车载DVR/NVR、布控球、智能眼镜、智能手电、智能电子工牌、无人机4G补传系统等统…

介绍目标检测中mAP50和mAP50-95的区别

在目标检测任务中&#xff0c;mAP&#xff08;mean Average Precision&#xff09;是一个常用的性能评估指标&#xff0c;用于衡量模型在不同类别和不同IoU&#xff08;Intersection over Union&#xff09;阈值下的平均精度。mAP50和mAP50-95是mAP的两个特定版本&#xff0c;它…

uniapp封装movable-area+movable-view组件,实现悬浮按钮可拖动,自动吸附边缘效果,自动向两边靠拢

兼容H5、App、微信小程序 子组件 /components/ShopCar/ShopCar.vue <template><view class"ShopCar"><movable-area class"movableArea" v-if"isShow"><movable-view class"movableView" :position"posi…

Qt的程序如何打包详细教学

生成Release版的程序 在打包Qt程序时&#xff0c;我们需要将发布程序需要切换为Release版本&#xff08;Debug为调试版本&#xff09;&#xff0c;编译器会对生成的Release版可执行程序进行优化&#xff0c;使生成的可执行程序会更小。 debug版本 debug版本是一种开发过程中的…