一、Redisson 详解
1、Redisson 介绍
Redisson 是一个 Redis 客户端,并且 Redisson 功能强大,所以使用 Redisson 可以很方便实现 Redis 分布式锁。关于分布式锁的更多知识可以参考我的另一篇博客:【Redis】之分布式锁
基于 Redis 实现的分布式锁存在一个锁的续期问题:持有锁的线程在锁过期时间内还没有执行完业务,此时锁超时被自动释放,这样会导致多个线程同时持有锁的问题,所以需要给锁的过期时间进行续期。
而 Redisson 就是能够很好的给我们解决锁的续期问题,同时 Redisson 还给我们实现了各种各样的锁,比如红锁(RedLock)、 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、读写锁(ReadWriteLock)、 信号量(Semaphore)。
更多关于 Redisson 介绍可以参考官方文档:Redisson的分布式锁和同步器
2、Redisson 原理
2-1、看门狗机制
Redisson 实现锁的续期功能使用的是看门狗机制,具体原理是:Redisson 在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁 key 的生存时间:
2-2、加锁机制:
- 线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库;
- 线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
2-3、watchdog 自动延期机制:
redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。
加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。
那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了呗。
需要注意:
- 看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的;
- 如果使用 Redisson 进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效;
如何理解锁的失效时间和锁的过期时间?
- 过期时间:这是在 Redis 设置的时间,时间到了之后会由 Redis 自身删除;
- 失效时间:在使用 Redisson 加锁时,会设置一个失效时间,这个时间是给 Redisson 在时间到了之后主动解锁用的。
所以我们如果要使用 Redisson 的自动续期功能的话,就不能设置锁的过期时间,只需要设置失效时间即可,锁的解除就交给 Redisson 执行。
2-4、Redisson 分布式锁的关键点总结:
- 对 key 不设置过期时间,由 Redisson 在加锁成功后给维护一个 watchdog 看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效;
- 通过Lua脚本实现了加锁和解锁的原子操作;
- 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。
二、Redisson 实战
1、项目整合 Redisson
1-1、引入依赖
<!-- 使用Redisson作为所有分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
1-2、配置 Redisson 客户端对象
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
// 1、创建配置
Config config = new Config();
// Redis url should start with redis:// or rediss://
config.useSingleServer().setAddress("redis://192.168.56.10:6379");f
// 2、根据Config创建出RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
配置好后就可以通过依赖注入方式创建 Redisson 客户端对象:
@Autowired
private RedissonClient redissonClient;
2、Redisson 加锁
2-1、普通加锁
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
// 1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
// 2、加锁,阻塞式等待,默认加的锁都是30s
myLock.lock();
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); }
catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
// 3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
设置锁的到期时间:
// 设置10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
myLock.lock(10,TimeUnit.SECONDS);
问题:在锁时间到了以后,如何自动续期
- 1)、如果我们传递了锁的超时时间,就会发送给 redis 执行脚本,进行占锁,默认超时就是我们制定的时间;
- 2)、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】,只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒:internalLockLeaseTime =【看门狗时间】 / 3 = 10s
2-2、读写锁
// 测试写锁
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); // 构造读写锁
RLock rLock = readWriteLock.writeLock(); // 获取写锁
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
// 测试读锁
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.readLock();
try {
// 加读锁
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
读写锁保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁,写锁没释放读锁必须等待:
- 读锁 + 读操作:相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁,他们都会同时加锁成功;
- 写锁 + 读操作:必须等待写锁释放;
- 写锁 + 写操作:阻塞方式;
- 读锁 + 写操作:有读锁,写也要等待。
总结就是:加读锁,可以读不能写;加写锁,既不能读也不能写。
2-3、信号量
/**
* 信号量也可以做分布式限流
*/
public String park() throws InterruptedException {
// 构建信号量
RSemaphore park = redisson.getSemaphore("park");
// 获取一个信号、获取一个值,redis 中的 park 值减一
park.acquire(); // 阻塞
boolean flag = park.tryAcquire(); // 非阻塞
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); // 释放一个信号量,redis 中的 park 值加一
return "ok";
}
2-4、闭锁
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
更多关于锁的资料可以参考官方文档:分布式锁和同步器。