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客户端为例);
public static void doSomething() {
// 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{} 就没用了,如下:
public static void doSomething() {
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秒之后自动释放,其他线程就可以获取到锁,接班干活了;
public static final String lock_key = "zjt-lock";
public void lock() {
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.html
SET key value [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中的锁。
标签:yyds,逻辑,lock,Redis,try,finally,线程,打开方式,分布式 From: https://blog.51cto.com/u_11365839/6957171