为什么要有分布式锁?
Redis避坑指南:为什么要有分布式锁?
作者:京东保险 张江涛
1、为什么要有分布式锁?
JUC提供的锁机制,可以保证在同一个JVM进程中同一时刻只有一个线程执行操作逻辑;
多服务多节点的情况下,就意味着有多个JVM进程,要做到这样,就需要有一个中间人;
分布式锁就是用来保证在同一时刻,仅有一个JVM进程中的一个线程在执行操作逻辑;
换句话说,JUC的锁和分布式锁都是一种保护系统资源的措施。尽可能将并发带来的不确定性转换为同步的确定性;
2、分布式锁特性(五大特性 非常重要)
特性1:互斥性。在任意时刻,只有一个客户端能持有锁。
特性2: 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
特性3: 解铃还须系铃人。加锁和解锁必须是同一个客户端(线程),客户端自己不能把别人加的锁给解了。
特性4:可重入性。同一个现线程已经获取到锁,可再次获取到锁。
特性5: 具有容错性。只要大部分的分布式锁节点正常运行,客户端就可以加锁和解锁。
2-1 常见分布式锁的三种实现方式
1. 数据库锁;2. 基于ZooKeeper的分布式锁;3. 基于Redis的分布式锁。
2-2 本文我们主要聊 redis实现分布式锁:
一个 setnx 就行了?value没意义?还有人认为 incr 也可以?再加个超时时间就行了?
3、分布式锁特性2之不会发生死锁
很多线程去上锁,谁锁成功谁就有权利执行操作逻辑,其他线程要么直接走抢锁失败的逻辑,要么自旋尝试抢锁;
• 比方说 A线程竞争到了锁,开始执行操作逻辑(代码逻辑演示中,使用 Jedis客户端为例);
publicstaticvoiddoSomething() {// RedisLock是封装好的一个类RedisLock redisLock = new RedisLock(jedis); // 创建jedis实例的代码省略,不是重点try {redisLock.lock(); // 上锁// 处理业务System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑中...");Thread.sleep(2000);System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑完毕");redisLock.unlock(); // 释放锁} catch (Exception e) {e.printStackTrace();}
}
• 正常情况下,A 线程执行完操作逻辑后,应该将锁释放。如果说执行过程中抛出异常,程序不再继续走正常的释放锁流程,没有释放锁怎么办?所以我们想到:
• 释放锁的流程一定要在 finally{} 块中执行,当然,上锁的流程一定要在 finally{} 对应的 try{} 块中,否则 finally{} 就没用了,如下:
publicstaticvoiddoSomething() {RedisLock redisLock = new RedisLock(jedis); // 创建jedis实例的代码省略,不是重点try {redisLock.lock(); // 上锁,必须在 try{}中// 处理业务System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑中...");Thread.sleep(2000);System.out.println(Thread.currentThread().getName() + " 线程处理业务逻辑完毕");} catch (Exception e) {e.printStackTrace();} finally {redisLock.unlock(); // 在finally{} 中释放锁}
}
写法注意: redisLock.lock(); 上分布式锁,必须在 try{}中。
在JAVA多线程中 lock.lock(); 单机多线程加锁操作需要在try{}之前。
3-1 redisLock.unlock() 放在 finally{} 块中就行了吗?还需要设置超时时间
如果在执行 try{} 中逻辑的时候,程序出现了 System.exit(0); 或者 finally{} 中执行异常,比方说连接不上 redis-server了;或者还未执行到 finally{}的时候,JVM进程挂掉了,服务宕机;这些情况都会导致没有成功释放锁,别的线程一直拿不到锁,怎么办?如果我的系统因为一个节点影响,别的节点也都无法正常提供服务了,那我的系统也太弱了。所以我们想到必须要将风险降低,可以给锁设置一个超时时间,比方说 1秒,即便发生了上边的情况,那我的锁也会在 1秒之后自动释放,其他线程就可以获取到锁,接班干活了;
publicstatic final String lock_key = "zjt-lock";publicvoidlock() { while (!tryLock()) {try {Thread.sleep(50); // 在while中自旋,如果说读者想设置一些自旋次数,等待最大时长等自己去扩展,不是此处的重点} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("线程:" + threadName + ",占锁成功!★★★");}private boolean tryLock() {SetParams setParams = new SetParams();setParams.ex(1); // 超时时间1ssetParams.nx(); // nxString response = jedis.set(lock_key, "", setParams); // 转换为redis命令就是:set zjt-key "" ex 1 nxreturn"OK".equals(response);}
注意,上锁的时候,设置key和设置超时时间这两个操作要是原子性的,要么都执行,要么都不执行。
Redis原生支持:
// http://redis.io/commands/set.htmlSETkeyvalue[EX seconds][PX milliseconds][NX|XX]
不要在代码里边分两次调用:
set k v
exipre k time
3-2 锁的超时时间该怎么计算?
刚才假设的超时时间 1s是怎么计算的?这个时间该设多少合适呢?
锁中的业务逻辑的执行时间,一般是我们在测试环境进行多次测试,然后在压测环境多轮压测之后,比方说计算出平均的执行时间是 200ms,锁的超时时间放大3-5倍,比如这里我们设置为 1s,为啥要放大,因为如果锁的操作逻辑中有网络 IO操作,线上的网络不会总一帆风顺,我们要给网络抖动留有缓冲时间。另外,如果你设置 10s,果真发生了宕机,那意味着这 10s中间,你的这个分布式锁的服务全部节点都是不可用的,这个和你的业务以及系统的可用性有挂钩,要衡量,要慎重(后边3-13会再详细聊)。那如果一个节点宕机之后可以通知 redis-server释放锁吗?注意,我是宕机,不可控力,断电了兄弟,通知不了的。
回头一想,如果我是优雅停机呢,我不是 kill -9,也不是断电,这样似乎可以去做一些编码去释放锁,你可以参考下 JVM的钩子、Dubbo的优雅停机、或者 linux进程级通信技术来做这件事情。当然也可以手动停服务后,手动删除掉 redis中的锁。
4、分布式锁特性3:解铃还须系铃人
如果说 A线程在执行操作逻辑的过程中,别的线程直接进行了释放锁的操作,是不是就出问题了?
什么?别的线程没有获得锁却直接执行了释放锁??现在是 A线程上的锁,那肯定只能 A线程释放锁呀!别的线程释放锁算怎么回事?联想 ReentrantLock中的 isHeldByCurrentThread()方法,所以我们想到,必须在锁上加个标记,只有上锁的线程 A线程知道,相当于是一个密语,也就是说释放锁的时候,首先先把密语和锁上的标记进行匹配,如果匹配不上,就没有权利释放锁;
private boolean tryLock() {SetParams setParams = new SetParams();setParams.ex(1); // 超时时间1ssetParams.nx(); // nxString response = jedis.set(lock_key, "", setParams); // 转换为redis命令就是:set zjt_key "" ex 1 nxreturn"OK".equals(response);}// 别的线程直接调用释放锁操作,分布式锁崩溃!publicvoidunlock() {jedis.del(encode(lock_key));System.out.println("线程:" + threadName + " 释放锁成功!☆☆☆");}privatebyte[] encode(String param) {return param.getBytes();}
4-1 这个密语value(约定)设置成什么呢?
很多同学说设置成一个 UUID就行了,上锁之前,在该线程代码中生成一个 UUID,将这个作为秘钥,存在锁键的 value中,释放锁的时候,用这个进行校验,因为只有上锁的线程知道这个秘钥,别的线程是不知道的。这个可行吗,当然可行。
String releaseLock_lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] \n" + "then\n" + " return redis.call(\"del\", KEYS[1])\n" + "else\n" + " return 0\n" + "end";privatebooleantryLock(String uuid) {SetParams setParams = newSetParams();setParams.ex(1); // 超时时间1ssetParams.nx(); // nxString response = jedis.set(lock_key, uuid, setParams); // 转换为redis命令就是:set zjt-key "" ex 1 nxreturn"OK".equals(response);}publicvoidunlock(String uuid) {List<byte[]> keys = Arrays.asList(encode(lock_key));List<byte[]> args = Arrays.asList(encode(uuid));// 使用lua脚本,保证原子性long eval = (Long) jedis.eval(encode(releaseLock_lua), keys, args);if (eval == 1) {System.out.println("线程:" + threadName + " 释放锁成功!☆☆☆");} else {System.out.println("线程:" + threadName + " 释放锁失败!该线程未持有锁!!!");}}private byte[] encode(String param) {return param.getBytes();}
为什么使用 lua脚本?因为保证原子性
因为是两个操作,如果分两步那就是:
get k // 进行秘钥 value的比对
del k // 比对成功后,删除k
如果第一步比对成功后,第二步还没来得及执行的时候,锁到期,然后紧接着别的线程获取到锁,里边的 uuid已经变了,也就是说持有锁的线程已经不是该线程了,此时再执行第二步的删除锁操作,肯定是错误的了。
5.分布式锁特性4之可重入性
作为一把锁,我们在使用 synchronized、ReentrantLock的时候是不是有可重入性?
那咱们这把分布式锁该如何实现可重入呢?如果 A线程的锁方法逻辑中调用了 x()方法,x()方法中也需要获取这把锁,按照这个逻辑,x()方法中的锁应该重入进去即可,那是不是需要将刚才生成的这个 UUID秘钥传递给 x()方法?怎么传递?用参数传递就会侵入业务代码
5-1 不侵入业务代码实现可重入:Thread-Id
我们主要是想给上锁的 A线程设置一个只有它自己知道的秘钥,把思路时钟往回拨,想想:
线程本身的 id(Thread.currentThread().getId())是不是就是一个唯一标识呢?我们把秘钥 value设置为线程的 id不就行了。
String releaseLock_lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] \n" + "then\n" + " return redis.call(\"del\", KEYS[1])\n" + "else\n" + " return 0\n" + "end";String addLockLife_lua = "if redis.call(\"exists\", KEYS[1]) == 1\n" + "then\n" + " return redis.call(\"expire\", KEYS[1], ARGV[1])\n" + "else\n" + " return 0\n" + "end";publicvoidlock() {// 判断是否可重入if (isHeldByCurrentThread()) {return;}while (!tryLock()) {try {Thread.sleep(50); // 自旋} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("线程:" + threadName + ",占锁成功!★★★");}// 是否是当前线程占有锁,同时将超时时间重新设置,这个很重要,同样也是原子操作privatebooleanisHeldByCurrentThread() {List<byte[]> keys = Arrays.asList(encode(lock_key));List<byte[]> args = Arrays.asList(encode(String.valueOf(threadId)), encode(String.valueOf(1)));long eval = (Long) jedis.eval(encode(addLockLife_lua), keys, args);returneval == 1;}privatebooleantryLock(String uuid) {SetParams setParams = newSetParams();setParams.ex(1); // 超时时间1ssetParams.nx(); // nxString response = jedis.set(lock_key, String.valueOf(threadId), setParams); // 转换为redis命令就是:set zjt-key xxx ex 1 nxreturn"OK".equals(response);}publicvoidunlock(String uuid) {List<byte[]> keys = Arrays.asList(encode(lock_key));List<byte[]> args = Arrays.asList(encode(String.valueOf(threadId)));// 使用lua脚本,保证原子性long eval = (Long) jedis.eval(encode(releaseLock_lua), keys, args);if (eval == 1) {System.out.println("线程:" + threadName + " 释放锁成功!☆☆☆");} else {System.out.println("线程:" + threadName + " 释放锁失败!该线程未持有锁!!!");}}private byte[] encode(String param) {return param.getBytes();}
5-2 Thread-Id 真能行吗?不行。
想想,我们说一个 Thread的id是唯一的,是在同一个 JVM进程中,是在一个操作系统中,也就是在一个机器中。而现实是,我们的部署是集群部署,多个实例节点,那意味着会存在这样一种情况,S1机器上的线程上锁成功,此时锁中秘钥 value是线程id=1,如果说同一时间 S2机器中,正好线程id=1的线程尝试获得这把锁,比对秘钥发现成功,结果也重入了这把锁,也开始执行逻辑,此时,我们的分布式锁崩溃!怎么解决?我们只需要在每个节点中维护不同的标识即可,怎么维护呢?应用启动的时候,使用 UUID生成一个唯一标识 APP_ID,放在内存中(或者使用zookeeper去分配机器id等等)。此时,我们的秘钥 value这样存即可:APP_ID+ThreadId
// static变量,final修饰,加载在内存中,JVM进程生命周期中不变privatestatic final StringAPP_ID = UUID.randomUUID().toString();String releaseLock_lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] \n" + "then\n" + " return redis.call(\"del\", KEYS[1])\n" + "else\n" + " return 0\n" + "end";String addLockLife_lua = "if redis.call(\"exists\", KEYS[1]) == 1\n" + "then\n" + " return redis.call(\"expire\", KEYS[1], ARGV[1])\n" + "else\n" + " return 0\n" + "end";publicvoidlock() {// 判断是否可重入if (isHeldByCurrentThread()) {return;}while (!tryLock()) {try {Thread.sleep(50); // 自旋} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("线程:" + threadName + ",占锁成功!★★★");}// 是否是当前线程占有锁,同时将超时时间重新设置,这个很重要,同样也是原子操作privatebooleanisHeldByCurrentThread() {List<byte[]> keys = Arrays.asList(encode(lock_key));List<byte[]> args = Arrays.asList(encode(APP_ID + String.valueOf(threadId)), encode(String.valueOf(1)));long eval = (Long) jedis.eval(encode(addLockLife_lua), keys, args);returneval == 1;}privatebooleantryLock(String uuid) {SetParams setParams = newSetParams();setParams.ex(1); // 超时时间1ssetParams.nx(); // nxString response = jedis.set(lock_key, APP_ID + String.valueOf(threadId), setParams); // 转换为redis命令就是:set zjt-key xxx ex 1 nxreturn"OK".equals(response);}publicvoidunlock(String uuid) {List<byte[]> keys = Arrays.asList(encode(lock_key));List<byte[]> args = Arrays.asList(encode(APP_ID + String.valueOf(threadId)));// 使用lua脚本,保证原子性long eval = (Long) jedis.eval(encode(releaseLock_lua), keys, args);if (eval == 1) {System.out.println("线程:" + threadName + " 释放锁成功!☆☆☆");} else {System.out.println("线程:" + threadName + " 释放锁失败!该线程未持有锁!!!");}}private byte[] encode(String param) {return param.getBytes();}
5-3 APP_ID(实例唯一标识) + ThreadId 还是 UUID 好呢?
继续听我说,如果 A线程执行逻辑中间开启了一个子线程执行任务,这个子线程任务中也需要重入这把锁,因为子线程获取到的线程 id不一样,导致重入失败。那意味着需要将这个秘钥继续传递给子线程,JUC中 InheritableThreadLocal 派上用场,但是感觉怪怪的,因为线程间传递的是父线程的 id。
微服务中多服务间调用的话可以借用系统自身有的 traceId作为秘钥即可。比如sgm中的traceId 或者 利用RPC框架的隐式传参
「至于选择哪种 value的方式,根据实际的系统设计 + 业务场景,选择最合适的即可,没有最好,只有最合适。」
5-4、锁重入的超时时间怎么设置?
注意,我们上边的主要注意力在怎么重入进去,而我们这是分布式锁,要考虑的事情还有很多,重入进去后,超时时间随便设吗?
比方说 A线程在锁方法中调用了 x()方法,而 x()方法中也有获取锁的逻辑,如果 A线程获取锁后,执行过程中,到 x()方法时,这把锁是要重入进去的,但是请注意,这把锁的超时时间如果小于第一次上锁的时间,比方说 A线程设置的超时时间是 1s,在 100ms的时候执行到 x()方法中,而 x()方法中设置的超时时间是 100ms,那么意味着 100ms之后锁就释放了,而这个时候我的 A线程的主方法还没有执行完呢!却被重入锁设置的时间搞坏了!这个怎么搞?
如果说我在内存中设置一个这把锁设置过的最大的超时时间,重入的时候判断下传进来的时间,我重入时 expire的时候始终设置成最大的时间,而不是由重入锁随意降低锁时间导致上一步的主锁出现问题
放在内存中行吗?我们上边举例中,调用的 x()方法是在一个 JVM中,如果是调用远程的一个 RPC服务呢(像这种调用的话就需要将秘钥value通过 RpcContext传递过去了)到另一个节点的服务中进行锁重入,这个时间依然是要用当前设置过锁的最大时间的,所以这个最大的时间要存在 redis中而非 JVM内存中
经过这一步的分析,我们的重入 lua脚本就修改为这样了:
ADD_LOCK_LIFE("if redis.call(\"get\", KEYS[1]) == ARGV[1]\n"+ // 判断是否是锁持有者"then\n"+ " local thisLockMaxTimeKeepKey=KEYS[1] .. \":maxTime\"\n"+// 记录锁最大时间的key是:锁名字:maxTime" local nowTime=tonumber(ARGV[2])\n"+// 当前传参进来的time" local maxTime=redis.call(\"incr\", thisLockMaxTimeKeepKey)\n"+// 取出当前锁设置的最大的超时时间,如果这个保持时间的key不存在返回的是字符串nil,这里为了lua脚本的易读性,用incr操作,这样读出来的都是number类型的操作" local bigerTime=maxTime\n"+// 临时变量bigerTime=maxTime" if nowTime>maxTime-1\n"+// 如果传参进来的时间>记录的最大时间" then\n"+ " bigerTime=nowTime\n"+// 则更新bigerTime" redis.call(\"set\", thisLockMaxTimeKeepKey, tostring(bigerTime))\n"+// 设置超时时间为最大的time,是最安全的" else \n"+ " redis.call(\"decr\", thisLockMaxTimeKeepKey)\n"+// 当前传参time<maxTime,将刚才那次incr减回来" end\n"+ " return redis.call(\"expire\", KEYS[1], tostring(bigerTime))\n"+// 重新设置超时时间为当前锁过的最大的time"else\n"+ " return 0\n"+ "end"),
其实,还有另外一种方案比较简单,就是锁的超时时间=第一次上锁的时间+后面所有重入锁的时间。也就是(expire = 主ttl + 重入exipre),这种方案是放大的思想,一放大就又有上边提到过的一个问题:expire太大怎么办,参考上边。
5-5、重入锁的方法中直接执行 unlock?考虑重入次数
A线程执行一共需要500ms,执行中需要调用 x()方法,x()方法中有一个重入锁,执行用了 50ms,然后执行完后,x()方法的 finally{} 块中将锁进行释放。
为啥能释放掉?因为秘钥我有,匹配成功了我就直接释放了。
这当然是有问题的,所以我们要通过锁重入次数来进行释放锁时候的判断,也就是说上锁的时候需要多维护一个 key来保存当前锁的重入次数,如果执行释放锁时,先进行重入次数 -1,-1后如果是0,可以直接 del,如果>0,说明还有重入的锁在,不能直接 del。
5-6 考虑如何存储锁的属性(锁的key 重入次数key 最大超时时间key)?
目前为止,算上上一步中设置最大超时时间的key,加上这一步重入次数的key,加上锁本身的key,已经有3个key,需要注意的事情是,这三个key的超时时间是都要设置的!为什么?假如说重入次数的 key没有设置超时时间,服务A节点中在一个JVM中重入了5次后,调用一次 RPC服务,RPC服务中同样重入锁,此时,锁重入次数是 6,这个时候A服务宕机,就意味着无论怎样,这把锁不可能释放了,这个分布式锁提供的完整能力,全线不可用了!
所以,这几个 key是要设置超时时间的!怎么设置?我上一个锁要维护这么多 key的超时时间?太复杂了吧,多则乱,则容易出问题。怎么办?我们想一下,是不是最大超时时间的 key和重入次数的 key,都附属于锁,它们都是锁的属性,如果锁不在了,谈它们就毫无意义,这个时候用什么存储呢?redis的 hash数据结构,就可以做,key是锁,里边的 hashKey分别是锁的属性, hashValue是属性值,超时时间只设置锁本身 key就可以了。这个时候,我们的锁的数据结构就要改变一下了。

6、如何解决过期时间确定和业务执行时长不确定性的问题:看门狗机制
3-2中设置超时时间那里,我们预估锁方法执行时间是 200ms,我们放大 5倍后,设置超时时间是 1s(过期时间确定)。假想一下,如果生产环境中,锁方法中的 IO操作,极端情况下超时严重,比方说 IO就消耗了 2s(业务执行时长不确定),那就意味着,在这次 IO还没有结束的时候,我这把锁已经到期释放掉了,就意味着别的线程趁虚而入,分布式锁崩溃!
我们要做的是一把分布式锁,想要的目的是同一时刻只有一个线程持有锁,作为服务而言,这个锁现在不管是被哪个线程上锁成功了,我服务应该保证这个线程执行的安全性,怎么办?锁续命(看门狗机制)。什么意思,一旦这把锁出现了上锁操作,就意味着这把锁开始投入使用,这时我的服务中需要有一个 daemon线程定时去守护我的锁的安全性,怎么守护?比如说锁超时时间设置的是 1s,那么我这个定时任务是每隔 300ms去 redis服务端做一次检查,如果我还持有,你就给我续命,就像 session会话的活跃机制一样。看个例子,我上锁时候超时时间设置的是 1s,实际方法执行时间是 3s,这中间我的定时线程每隔 300ms就会去把这把锁的超时时间重新设置为 1s,每隔 300ms一次,成功将锁续命成功。
publicclassRedisLockIdleThreadPool {private String threadAddLife_lua = "if redis.call(\"exists\", KEYS[1]) == 1\n" + "then\n" + " return redis.call(\"expire\", KEYS[1], ARGV[1])\n" + "else\n" + " return 0\n" + "end";privatevolatile ScheduledExecutorService scheduledThreadPool;publicRedisLockIdleThreadPool() {if (scheduledThreadPool == null) {synchronized (this) {if (scheduledThreadPool == null) {scheduledThreadPool = Executors.newSingleThreadScheduledExecutor(); // 我这样创建线程池是为了代码的易读性,大家务必使用ThreadPoolExecutor去创建scheduledThreadPool.scheduleAtFixedRate(() -> {addLife();}, 0, 300, TimeUnit.MILLISECONDS);}}}}privatevoidaddLife() {// ... 省略jedis的初始化过程List<byte[]> keys = Arrays.asList(RedisLock.lock_key.getBytes());List<byte[]> args = Arrays.asList(String.valueOf(1).getBytes());jedis.eval(threadAddLife_lua.getBytes(), keys, args);}}
这就行吗?还不行!
为啥?想一下,如果每个服务中都像这样去续命锁,假如说A服务还在执行过程中的时候,还没有执行完,就是说还没有手动释放锁的时候,宕机,此时 redis中锁还在有效期。服务B 也一直在续命这把锁,此时这把锁一直在续命,但是 B的这个续命一直续的是 A当时设的锁,这不是扯吗?我自己在不断续命,导致我的服务上一直获取不到锁,实际上 A已经宕机了呀!该释放了,不应该去续命了,这不是我服务 B该干的活!
续命的前提是,得判断是不是当前进程持有的锁,也就是我们的 APP_ID,如果不是就不进行续命。
续命锁的 lua脚本发生改变,如下:
THREAD_ADD_LIFE("local v=redis.call(\"get\", KEYS[1]) \n"+ // get key"if v==false \n"+// 如果不存在key,读出结果v是false"then \n"+ // 不存在不处理"else \n"+ " local match = string.find(v, ARGV[1]) \n"+// 存在,判断是否能和APP_ID匹配,匹配不上时match是nil" if match==\"nil\"\n"+ " then \n"+ " else \n"+ " return redis.call(\"expire\", KEYS[1], ARGV[2]) \n"+// 匹配上了返回的是索引位置,如果匹配上了意味着就是当前进程占有的锁,就延长时间" end \n"+ "end")
6-1 锁在我手里,我挂了,这... 没救。只能等待锁超时释放
即便设置了一个很合理的 expire,比如 10s,但是线上如果真出现了A节点刚拿到锁就宕机了,那其他节点也只能干等10s,之后才能拿到锁。主要还是业务能不能接受。而如果是 To C的业务中,大部分场景无法接受的,因为可能会导致用户流失。所以我们需要另外一个监控服务,定时去监控 redis中锁的获得者的健康状态,如果获取者超过n次无法通信,由监控服务负责将锁摘除掉,让别的线程继续去获取到锁去干活。
相关文章:

为什么要有分布式锁?
Redis避坑指南:为什么要有分布式锁?作者:京东保险 张江涛1、为什么要有分布式锁?JUC提供的锁机制,可以保证在同一个JVM进程中同一时刻只有一个线程执行操作逻辑;多服务多节点的情况下,就意味着有…...

【Redis】Redis持久化之RDB详解(Redis专栏启动)
📫作者简介:小明java问道之路,2022年度博客之星全国TOP3,专注于后端、中间件、计算机底层、架构设计演进与稳定性建工设优化。文章内容兼具广度深度、大厂技术方案,对待技术喜欢推理加验证,就职于知名金融公…...

Retinanet网络与focal loss损失
参考代码:https://github.com/yhenon/pytorch-retinanet 1.损失函数 1)原理 本文一个核心的贡献点就是 focal loss。总损失依然分为两部分,一部分是分类损失,一部分是回归损失。 在讲分类损失之前,我们来回顾一下二…...
Spring事务的失效场景
事务失效场景 方法用private或final修饰 Spring底层使用了AOP,而AOP的实现方式有两种,分别是JDK动态代理和CGLIB,JDK动态代理是实现抽象接口,CGLIB是继承父类,无论哪种方式,都需要重写方法来进行方法增强,而…...
芯动联科在科创板IPO过会:拟募资10亿元,金晓冬为实际控制人
2月13日,上海证券交易所披露的信息显示,安徽芯动联科微系统股份有限公司(下称“芯动联科”)获得科创板上市委会议审议通过。据贝多财经了解,芯动联科于2022年6月24日在科创板递交招股书。 本次冲刺上市,芯…...

数据结构之单链表
一、链表的组成 链表是由一个一个的节点组成的,节点又是一个一个的对象, 相邻的节点之间产生联系,形成一条链表。 例子:假如现在有两个人,A和B,A保存了B的联系方式,这俩人之间就有了联系。 A和…...
儿子跟妈妈关系不好怎么办?这里有解决办法!
15岁的男孩子正处于青春期,很多男孩都傲慢自大,听不进去别人的建议,以自己为中心,认为自己能处理好自己的事情,不想听父母的唠叨。母亲面对青春期的孩子也是举手无措,语气不好,会让孩子更叛逆。…...

论文投稿指南——中文核心期刊推荐(植物保护)
【前言】 🚀 想发论文怎么办?手把手教你论文如何投稿!那么,首先要搞懂投稿目标——论文期刊 🎄 在期刊论文的分布中,存在一种普遍现象:即对于某一特定的学科或专业来说,少数期刊所含…...
华科万维C++章节练习4_6
【程序设计】 题目: 编程输出下列图形,中间一行英文字母由输入得到。 A B B B C C C C C D D D D D D D C C C C C B B B A 开头空一格,字母间空两格…...

详解子网技术
一 : Internet地址 Intemet实质上是把分布在世界各地的各种网络如计算机局域网和广域网、数字数据通信网以及公用电话交换网等互相连接起来而形成的超级网络。但是 , 网络的物理地址给Internet统一全网地址带来两个方面的问题: 第一,物理地址是物理网络技术的一种…...

chatGTP的全称Chat Generative Pre-trained Transformer
chatGPT,有时候我会拼写为:chatGTP,所以知道这个GTP的全称是很有用的。 ChatGPT全名:Chat Generative Pre-trained Transformer ,中文翻译是:聊天生成预训练变压器,所以是GPT,G是生…...

hive数据存储格式
1、Hive存储数据的格式如下: 存储数据格式存储形式TEXTFILE行式存储SEQUENCEFILE行式存储ORC列式存储PARQUET列式存储 2、行式存储和列式存储 解释: 1、上图左面为逻辑表;右面第一个为行式存储,第二个温列式存储; …...

mysql数据库备份与恢复
mysql数据备份: 数据备份方式 物理备份: 冷备:.冷备份指在数据库关闭后,进行备份,适用于所有模式的数据库热备:一般用于保证服务正常不间断运行,用两台机器作为服务机器,一台用于实际数据库操作应用,另外…...
《NFL橄榄球》:辛辛那提猛虎·橄榄1号位
辛辛那提猛虎(英语:Cincinnati Bengals),又译辛辛那提孟加拉虎,是一支职业美式橄榄球球队位于俄亥俄州辛辛那提。他们现时为美联北区的其中一支球队,他们在1968年加入美国橄榄球联合会,并在1970…...

2、线程、块和网格
目录一、线程、块、网格概念二、代码分析2.1 打印第一个线程块的第一线程2.2 打印当前线程块的当前线程2.3 获取当前是第几个线程一、线程、块、网格概念 CUDA的软件架构由网格(Grid)、线程块(Block)和线程(Thread&am…...

C++ 算法主题系列之贪心算法的贪心之术
1. 前言 贪心算法是一种常见算法。是以人性之念的算法,面对众多选择时,总是趋利而行。 因贪心算法以眼前利益为先,故总能保证当前的选择是最好的,但无法时时保证最终的选择是最好的。当然,在局部利益最大化的同时&am…...

请注意,PDF正在传播恶意软件
据Bleeping Computer消息,安全研究人员发现了一种新型的恶意软件传播活动,攻击者通过使用PDF附件夹带恶意的Word文档,从而使用户感染恶意软件。 类似的恶意软件传播方式在以往可不多见。在大多数人的印象中,电子邮件是夹带加载了恶…...

【Kubernetes】【二】环境搭建 环境初始化
本章节主要介绍如何搭建kubernetes的集群环境 环境规划 集群类型 kubernetes集群大体上分为两类:一主多从和多主多从。 一主多从:一台Master节点和多台Node节点,搭建简单,但是有单机故障风险,适合用于测试环境多主…...
Python:每日一题之发现环(DFS)
题目描述 小明的实验室有 N 台电脑,编号 1⋯N。原本这 N 台电脑之间有 N−1 条数据链接相连,恰好构成一个树形网络。在树形网络上,任意两台电脑之间有唯一的路径相连。 不过在最近一次维护网络时,管理员误操作使得某两台电脑之间…...

C++设计模式(14)——享元模式
亦称: 缓存、Cache、Flyweight 意图 享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。 问题 假如你希望在长时间工作后放…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...

C# 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...