Redisson
简介
整合
Maven 依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置方法,使用该配置类创建 RedissonClient 实体
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://121.5.47.104:6379");
// 通过 config 创建 RedissonClient 对象
return Redisson.create(config);
}
}
测试 lock
@ResponseBody
@GetMapping("/hello")
public String hello(){
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
// 获取锁
RLock lock = redissonClient.getLock("lock");
// 加锁 阻塞式等待
lock.lock();
try {
System.out.println("加锁成功-执行业务-线程id:"
+Thread.currentThread().getId()+"-当前时间:"
+dateFormat.format(new Date()));
Thread.sleep(15000);
} catch (InterruptedException e) {
}finally {
// 解锁
System.out.println("释放锁-线程id:"
+Thread.currentThread().getId()+"-当前时间:"
+dateFormat.format(new Date()));
lock.unlock();
}
return "hello";
}
分布式情况下,打开两个相同的服务,端口不一样,一个 10001,一个 10002。
同时访问 各自端口下的 /hello 服务
可以看到,先访问的 10001 端口下的线程先占到锁,执行业务后释放,10002 端口下的线程才能占到锁,开始执行业务。
一个网页执行业务的时候,另一个网页转圈圈。
注意,上述代码并没有设置锁的过期时间,考虑一个场景,如果先获得锁的进程在执行业务的时候停电(这里用 stop 服务模拟),无法执行下面 finally 块里的释放锁。会不会造成死锁,另一个进程无法获得锁呢?
可以看到,端口 10001 下的线程获得锁没来得及释放就被停掉了,10002 下的线程在 30秒后获得了锁。
锁的自动续期 WatchDog
上述问题是因为在创建的时候默认添加了 30秒的过期时间。
当把模拟执行业务的时间加长到 60 s,发现还是可以正常加锁解锁
加锁成功-执行业务-线程id:392-当前时间:2021/08/04 11:01:51
释放锁-线程id:392-当前时间:2021/08/04 11:02:51
加锁成功-执行业务-线程id:390-当前时间:2021/08/04 11:02:51
释放锁-线程id:390-当前时间:2021/08/04 11:03:51
说明正在运行的业务可以自动续锁
可以使用下面操作来指定过期时间
void lock(long time, TimeUnit timeUnit);
这种方式指定了过期时间,redisson 将过期时间写入 lua 脚本让 redis 执行,一旦到达过期时间,直接释放锁,业务方法出现冲突自行解决。
void lock()
这种方式未指定过期时间,使用 LockWatchdogTimeout 看门狗的默认时间 30*1000 ms。
一旦占锁成功,启动一个定时任务,每隔 看门狗的默认时间/3 即 10s 执行去更新过期时间,直到业务方法完成,锁被释放。
读写锁 ReadWriteLock
类似 JUC 包下的读写锁,写锁是一个排它锁,读锁是一个共享锁。当拿到写锁修改数据时,所有读锁被阻塞。这样可以保证能够读到最新的数据。
读场景
@ResponseBody
@GetMapping("/readValue")
public String readValue(){
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String res = "";
// 获取读锁
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
RLock readLock = lock.readLock();
readLock.lock();
try {
// 读数据
System.out.println("获取了读锁-线程id:"
+Thread.currentThread().getId()
+"-"
+dateFormat.format(new Date()));
res = redisTemplate.opsForValue().get("value");
Thread.sleep(20000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("释放了读锁-线程id:"
+Thread.currentThread().getId()
+"-"
+dateFormat.format(new Date()));
// 释放锁
readLock.unlock();
}
return res;
}
写场景
@ResponseBody
@GetMapping("/writeValue")
public String writeValue(){
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String res = "";
// 获取写锁
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
RLock writeLock = lock.writeLock();
writeLock.lock();
try {
System.out.println("获取了写锁-线程id:"
+Thread.currentThread().getId()
+"-"
+dateFormat.format(new Date()));
res = UUID.randomUUID().toString();
// 写数据
redisTemplate.opsForValue().set("value", res);
Thread.sleep(20000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("释放了写锁-线程id:"
+Thread.currentThread().getId()
+"-"
+dateFormat.format(new Date()));
// 释放锁
writeLock.unlock();
}
return res;
}
打开多个网页,分别多个请求 /readValue 和 /writeValue
例:一个先请求 /writeValue 三个后请求 /readValue
结果
获取了写锁-线程id:384-2021/08/04 17:45:48
释放了写锁-线程id:384-2021/08/04 17:46:09
获取了读锁-线程id:385-2021/08/04 17:46:09
获取了读锁-线程id:386-2021/08/04 17:46:11
获取了读锁-线程id:389-2021/08/04 17:46:13
释放了读锁-线程id:385-2021/08/04 17:46:29
释放了读锁-线程id:386-2021/08/04 17:46:31
释放了读锁-线程id:389-2021/08/04 17:46:33
可以看到写锁为互斥锁,读锁为共享锁。
- 读 + 读:共享,并发
- 读 + 写:互斥,先读后写
- 写 + 读:互斥,先写后读
- 写 + 写:互斥
信号量 Semaphore
以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
模拟一下上述场景
停车场景
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
// 获取信号量(停车场) parking lot
RSemaphore parkingLot = redissonClient.getSemaphore("parkingLot");
// 获取一个车位,没有就在门口等
parkingLot.acquire();
return "ok";
}
离开场景
@ResponseBody
@GetMapping("/go")
public String go(){
// 获取信号量(停车场) parking lot
RSemaphore parkingLot = redissonClient.getSemaphore("parkingLot");
// 跟看门大爷说一声 车开走了
parkingLot.release();
return "ok";
}
向 redis 中插入一个 key 为 parkingLot(停车场) value 为 3(共3个车位)。
打开一个网页连续访问 /park 三次,第四次一直转圈圈。每次访问 redis 中的 parkingLot 值减 1 。
再打开一个网页访问 /go,打开之后之前转圈圈的 /park 访问成功,之后再访问 /go,redis 中的 parkingLot 值加 1。
可以看出使用 acquire() 的获取信号量是阻塞性的,没有就一直等,可以使用 tryAcquire(),没有就走了。
闭锁 CountDownLatch
CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch 能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。
模拟一个场景,停车场里停了 3 辆车。现在停车场倒闭了,需要关门,关门之前需要 3 辆车都离开才能关门。
关门场景
@ResponseBody
@GetMapping("/lockDoor")
public void lockDoor() throws InterruptedException {
// 获取闭锁(停车场)
RCountDownLatch parkingLot = redissonClient.getCountDownLatch("parkingLot");
// 清点了下 里面有三辆车
parkingLot.trySetCount(3);
System.out.println("有三辆车 ~ 等待离开");
// 等待它们离开
parkingLot.await();
System.out.println("车都走了 ~ 准备关门");
}
离开场景
@ResponseBody
@GetMapping("/leave/{car}")
public void leave(@PathVariable("car") String car){
// 获取闭锁(停车场)
RCountDownLatch parkingLot = redissonClient.getCountDownLatch("parkingLot");
// 离开一辆
parkingLot.countDown();
System.out.println(car+" 离开了停车场");
}
结果,打开一个网页访问 /lockDoor,一直转圈圈,另一个网页访问三次 /leave。关门结束。
有三辆车 ~ 等待离开
宝马 离开了停车场
红旗 离开了停车场
五菱宏光 离开了停车场
车都走了 ~ 准备关门