为什么要有分布式锁?

news2024/11/26 6:12:28

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); // 超时时间1s
		setParams.nx();  // nx
		String response = jedis.set(lock_key, "", setParams); // 转换为redis命令就是:set zjt-key "" ex 1 nx
		return"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); // 超时时间1s
		setParams.nx();  // nx
		String response = jedis.set(lock_key, "", setParams); // 转换为redis命令就是:set zjt_key "" ex 1 nx
		return"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); // 超时时间1s
		setParams.nx();  // nx
		String response = jedis.set(lock_key, uuid, setParams); // 转换为redis命令就是:set zjt-key "" ex 1 nx
		return"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); // 超时时间1s
		setParams.nx();  // nx
		String response = jedis.set(lock_key, String.valueOf(threadId), setParams); // 转换为redis命令就是:set zjt-key xxx ex 1 nx
		return"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); // 超时时间1s
		setParams.nx();  // nx
		String response = jedis.set(lock_key, APP_ID + String.valueOf(threadId), setParams); // 转换为redis命令就是:set zjt-key xxx ex 1 nx
		return"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次无法通信,由监控服务负责将锁摘除掉,让别的线程继续去获取到锁去干活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345637.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023最新Java面试手册(性能优化+微服务架构+并发编程+开源框架)

Java面试手册 一、性能优化面试专栏 1.1、 tomcat性能优化整理 1.2、JVM性能优化整理 1.3、Mysql性能优化整理 二、微服务架构面试专栏 2.1、SpringCloud面试整理 2.2、SpringBoot面试整理 2.3、Dubbo面试整理 三、并发编程高级面试专栏 四、开源框架面试题专栏 4.1、Sprin…

勒索软件、网络钓鱼、零信任和网络安全的新常态

当疫情来袭时&#xff0c;网络罪犯看到了他们的机会。随着公司办公、政府机构、学校和大学从以往的工作模式转向远程线上办公模式&#xff0c;甚至许多医疗保健设施都转向线上&#xff0c;这种快速的过渡性质导致了不可避免的网络安全漏洞。消费者宽带和个人设备破坏了企业安全…

name不能作为js的变量名

今天看红宝书的时候&#xff0c;看中到&#xff1a; null和undefined值没有toString()方法&#xff0c;而String()却能够将null和undefined转化成对应的字符串。 想着去尝试一下&#xff0c;用到了name作为变量值&#xff0c;发现让name为null或undefined的时候&#xff0c;就…

(C00102)基于Servlet的在线服装销售管理系统——有文档

基于Servlet的在线服装销售管理系统项目简介项目获取开发环境项目技术运行截图项目简介 本项目是基于J2EE的servlet的在线服装销售管理系统&#xff0c;衣服、裤子、上衣等等&#xff0c;本项目有三种权限&#xff1a;游客、用户、管理员 游客&#xff1a;服装浏览、加入购物车…

“鸡血驱动”为CS:GO、LOL注入“强心剂”!英特尔锐炫A750显卡实测

自推出锐炫系独显以来&#xff0c;英特尔一直在为自家的独显产品的市场竞争力添砖加瓦&#xff0c;其中就包括了驱动程序的持续更新优化和市场策略的调整。在驱动部分&#xff0c;在去年底31.0.101.3802版的小优化更新之后&#xff0c;今年2月初又陆续更新了31.0.101.4091 WHQL…

手把手教你二进制安装生产环境 K8s 多 master 节点高可用集群详细图文教程

目录 一、集群环境准备 1.1 kubeadm 和二进制安装 k8s 适用场景分析 1.2 多 master 节点高可用架构图 二、基础环境配置&#xff08;以下操作所有节点都得执⾏&#xff09; 2.1 初步的环境初始化 2.2 关闭交换分区 swap 提升性能 2.3 修改机器内核参数 2.4 配置阿里云…

深入理解 Handler(java 层 + native 层)

文章目录回顾线程消息队列时怎样实现的消息是怎么传递的&#xff1f;Handle 的延迟消息是怎么处理的&#xff1f;IdleHandler 的原理主线程进入了 Looper 循环为什么没有 ANR&#xff1f;消息屏障是什么&#xff1f;回顾 之前学习过Handler相关的基础知识&#xff0c;今天再学…

ESP-IDF + Vscode ESP32 开发环境搭建以及开发入门

ESP-IDF Vscode ESP32 开发环境搭建以及开发入门 文章目录ESP-IDF Vscode ESP32 开发环境搭建以及开发入门1. 前言2. 下载开发工具3. 配置工具4. 创建工程5. 解决vscode找不到头文件&#xff0c;波浪线警告6. 添加自己的组件6.1 组件说明6.2 添加项目组件6.3 添加扩展组件7. …

Python进阶篇(一)-- Django快速上手

1 Django概述 Web框架&#xff0c;就是用于开发Web服务器端应用的基础设施&#xff0c;说得通俗一点就是一系列封装好的模块和工具。事实上&#xff0c;即便没有Web框架&#xff0c;我们仍然可以通过socket或CGI来开发Web服务器端应用&#xff0c;但是这样做的成本和代价在商业…

Stable Diffusion 1 - 初始跑通 文字生成图片

文章目录关于 Stable DiffusionLexica代码实现安装依赖库登陆 huggingface查看 huggingface token下载模型计算生成设置宽高测试迭代次数生成多列图片关于 Stable Diffusion A latent text-to-image diffusion model Stable Diffusion 是一个文本到图像的潜在扩散模型&#xff…

撕开市场缺口,认养一头牛“犟心”能给谁?

随着疫情防控政策优化&#xff0c;2023年以来中国消费力和投资活动均迎来复苏。其中&#xff0c;乳制品赛道受益于国内消费者健康消费理念的加强&#xff0c;呈现出稳步增长的势头。一方面&#xff0c;乳制品消费需求旺盛&#xff0c;市场未来可期。据中商研究院预计&#xff0…

【Hello Linux】Linux环境下写的第一个程序 -- 进度条

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;写出Linux中的第一个小程序 进度条 进度条小程序行缓冲区概念\r 和 \n进度条代码和演示行缓冲区概念 我们首先用两段代码来感受下行缓…

结合ENVI和PIE Hyp讲述高光谱遥感信息处理技术,包括光谱恢复、光谱库建立、光谱特征提取、混合像元分解、图像分类及精度检验

大气温室气体浓度不断增加&#xff0c;导致气候变暖加剧&#xff0c;随之会引发一系列气象、生态和环境灾害。如何降低温室气体浓度和应对气候变化已成为全球关注的焦点。海洋是地球上最大的“碳库”,“蓝碳”即海洋活动以及海洋生物&#xff08;特别是红树林、盐沼和海草&…

05 OpenCV色彩空间处理

色彩空间&#xff08;Color Space&#xff09;是一种用于描述颜色的数学模型&#xff0c;它将颜色表示为多维向量或坐标&#xff0c;通常由三个或四个独立的分量来表示。不同的色彩空间在颜色的表示方式、可表达颜色的范围、计算速度和应用场景等方面存在差异&#xff0c;不同的…

ChatGPT写代码水平惊艳到我,很性感但有点危险

这几天属实是被ChatGPT刷屏了&#xff0c;十年寒窗无人问&#xff0c;一举成名天下知。不少人和ChatGPT对话后&#xff0c;都觉得自己像个傻逼。这位“最强懂哥”可以轻松应答各种问题&#xff0c;给出的答案不仅条理清晰&#xff0c;还会引用例子支撑观点。让它帮忙写程序&…

九、初识卷积

文章目录1、通过边缘检测认识卷积2、Padding3、Strid Convelution4、RGB图像的卷积THE END1、通过边缘检测认识卷积 \qquad在使用神经网络进行图像识别时&#xff0c;神经网络的前几层需要完成对图像的边缘检测任务&#xff0c;所谓的边缘检测就是让计算机识别出一张图片的垂直…

【智能计算数学】微积分

高数问题解决流程引例&#xff1a;回归回归引例&#xff1a;分类分类线性可分FLD线性不可分智能计算讨论范围下降法为什么要用下降法&#xff1f;- 解析解很难写出公式或很复杂难计算有哪些常用的下降法&#xff1f;- 梯度下降&高斯-牛顿法梯度下降&#xff08;Gradient De…

初步认识操作系统(Operator System)

操作系统一&#xff0c;冯诺依曼体系结构内存的重要作用二&#xff0c;操作系统的概念三&#xff0c;设计操作系统的目的三&#xff0c;操作系统在计算机体系中的定位四&#xff0c;操作系统是如何进行管理的一&#xff0c;冯诺依曼体系结构 在众多计算机相关的书籍中&#xff…

linux安装docker和Docker Compose

1、安装环境 此处在Centos7进行安装&#xff0c;可以使用以下命令查看CentOS版本 lsb_release -a 在 CentOS 7安装docker要求系统为64位、系统内核版本为 3.10 以上&#xff0c;可以使用以下命令查看 uname -r 2、用yum源安装 2.1 查看是否已安装docker列表 yum list inst…

Doom流量回放工具导致的测试环境服务接口无响应的排查过程

Doom流量回放工具导致的测试环境服务接口无响应的排查过程 现象描述&#xff1a; a)部分接口&#xff08;A组接口&#xff09;无响应 b)部分接口&#xff08;B组接口&#xff09;正常响应 c)还有一部分接口&#xff08;C组接口&#xff09;,场景1无响应&#xff0c;场景2正常响…