分布式锁
一. 引入Redisson
1.1 Redisson介绍
Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。
Redisson 可以提供分布式锁,延时队列,布隆过滤器等Redis高级功能。
1.2 Maven坐标
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
1.3 配置文件
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.31.11:6379");
RedissonClient redissonClient= Redisson.create(config);
return redissonClient;
}
}
二. 测试
1. 基本测试
@RestController
@RequestMapping("/testLock")
public class TestLockController {
@Autowired
private RedissonClient redisson;
@RequestMapping("/test1")
public String test1(){
//1. 阻塞获取一把名为 locak1 的锁
RLock lock = redisson.getLock("lock1");
System.out.println("正在获取锁...");
// 1.1 加锁 这里的加锁方式为 阻塞获取 直到获取锁才行
lock.lock();
// 1.2 自定义加锁时间
// lock.lock(25,TimeUnit.SECONDS);
// 1.3 尝试加锁 默认 最多 30秒
// lock.tryLock();
// ...
System.out.println("获取到锁,执行业务...");
try {
// 业务代码
Thread.sleep(10000);
} catch (Exception e) {
} finally {
// 2. 解锁
lock.unlock();
}
return "ok";
}
1.1 这个代码存在死锁么?
lock.lock() 是阻塞获取锁的,如果出现一种情况:在一个分布式系统下,获取到锁的服务器宕机了,没有及时的释放掉锁,那么获取锁的服务器会一直阻塞么
?
答案是 不会的
,这里涉及到 redisson 实现的一个 机制:看门狗
,看门狗
赋予了 lock()方法的 默认的加锁时间是30秒,如果获取到锁的服务器发生宕机,那么锁在30秒后会自动释放掉
,分布式系统非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
那么又衍生出了另外一种情况:如果业务代码执行时间超过了30秒
,这个锁被释放掉了,那么执行unlock() 释放锁 会发生什么?
这里有两种情况 第一种 lock() 方法没有自定义时间
,那加锁时间就是30秒, 看门狗
会每隔 10秒自动续期到 30秒(其实就是把剩余时间从20秒改到30秒),直到 finally 代码块 执行释放锁,如果期间 服务器宕机了就不用说了,宕机以后看门狗也不会工作了,最多30秒有效时间
第二种情况,lock方法自定义了加锁时间,
看门狗这个续时机制就不会触发,如果业务代码执行时间超过了自定义的时间后执行 unlock(),那么这个释放锁操作 是不会成功的,因为Redisson释放锁操作是个原子操作
,他会比较redis的存放的锁数据 , key = key AND value = value(这里是用lua脚本实现的),这两个条件成立时才会执行成功,最后系统会抛出异常。
1.2 看门狗原理
RLock点击进去
继承了JUC下面的lock类,怪不得他的api方法和 lock这么像,我在一篇文章里也写到了 Lock 有兴趣的可以看一下ReentrantLock原理
他的实现类:
实现类里面无参 的lock方法:
这里的这个lock方法传三个参数 1. 时间 2 时间单位 3 是否可被打断
继续:
加锁的实现也是lua脚本:
返回上一层:
renewExpiration方法:
获取到锁后,会启动一个定时任务,定时任务 在 30/3 = 10 秒后 启动:
到这里就能看到 看门狗是通过定时任务实现来实现续时的
,
unlock方法这里我就不演示了
三. 进阶
3.1 读写锁
@RestController
@RequestMapping("/testLock")
public class TestLockController {
@Autowired
private RedissonClient redisson;
@RequestMapping("/write")
public String writeValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock writeLock = lock.writeLock();
try {
System.out.println("正在获取写锁...");
writeLock.lock();
System.out.println("写锁加锁成功..." );
Thread.sleep(15000);
//redisTemplate.opsForValue().set("writeValue",s);
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
return "ok";
}
@RequestMapping("/read")
public String readValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock readLock = lock.readLock();
try {
System.out.println("正在获取读锁...");
readLock.lock();
System.out.println("读锁加锁成功...");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return "ok";
}
}
3.2 读写锁规律
- 读 + 读 相当于无锁,并发读,只会在 reids中记录好,所有当前的读锁,他们都会同时加锁成功
- 写 + 读 等待写锁释放
- 写 + 写 阻塞方式
- 读 + 写 有读锁,写也需要等待
- 只要有写的存在,都必须等待
3.3 闭锁
有这样一个场景,晚上保安大叔来教室锁门,正好当时教室有五个学生,保安大叔需要等这五个学生离开教室,才能锁门。这种场景可以用到 闭锁
@RestController
@RequestMapping("/testLock")
public class TestLockController {
@Autowired
private RedissonClient redisson;
/**
* 锁门
* @return
* @throws InterruptedException
*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
// 只有当 5变成 0的时候才能锁门
door.trySetCount(5);
door.await();//等待闭锁都完成
return "锁门成功...";
}
/**
* 离开教室
* @param name
* @return
*/
@GetMapping("/outRoom/{name}")
public String gogogo(@PathVariable("name") String name) {
RCountDownLatch door = redisson.getCountDownLatch("door");
// 计数器减一
door.countDown();
return name + " 离开教室";
}
}
3.4 信号量测试
/**
* 车库停车
* 3车位
* @return
*/
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redisson.getSemaphore("park");
// 占用一个车位
boolean isOk = park.tryAcquire();
return isOk + "";
}
@GetMapping("/out")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
//释放一个车位
park.release();
return "ok";
}