【redisson】redisson分布式锁原理分析
为什么要有分布式锁 分布式锁是基于什么延伸出来的?这个可以在博主首页搜索:超卖, 里面通过超卖例子说明分布式锁和传统锁的区别,本文重点阐述的是redisson的api使用及其源码分析。
文章目录
- springboot整合redisson
- redisson三个重要参数
- leaseTime
- waitTime
- lockWatchdogTimeout
- redisson常用api
- api源码简析
- leaseTime和watchDog不能共存的原因
- lock同步阻塞的原因
springboot整合redisson
<!-- redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.5</version>
</dependency>
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
// 创建 Redisson 配置
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
// 配置 Redis 连接地址(假设是单机版 Redis)
singleServerConfig.setAddress("redis://xx.xx.xx.xxx:6379");
singleServerConfig.setDatabase(1);
singleServerConfig.setPassword("xxxxxxx");
// 创建并返回 RedissonClient
return Redisson.create(config);
}
/**
* 哨兵模式配置
*/
// @Bean
// public RedissonClient createClient() {
// // 创建配置对象
// Config config = new Config();
//
// // 使用 JSON 编解码器
// config.setCodec(new JsonJacksonCodec());
//
// // 配置哨兵模式
// SentinelServersConfig sentinelServersConfig = config.useSentinelServers();
// // 配置哨兵地址,多个哨兵地址之间用逗号分隔
// sentinelServersConfig.setSentinelAddresses(Arrays.asList(
// "redis://xx.xx.xx.xxx:26379", "redis://xx.xx.xx.xxx:26380","redis://xx.xx.xx.xxx:26381"
// ));
//
// // 配置主 Redis 实例名称
// sentinelServersConfig.setMasterName("mymaster");
//
// // 配置数据库索引(Redis 默认是 0)
// sentinelServersConfig.setDatabase(0);
//
// // 配置哨兵认证密码
// sentinelServersConfig.setPassword("xxxxx");
//
// /**
// * 设置连接池的最大连接数、最小连接数等参数(可选)
// */
// // 设置连接超时为 10 秒
// sentinelServersConfig.setConnectTimeout(10000);
// // 设置操作超时为 3 秒
// sentinelServersConfig.setTimeout(3000);
// // 设置重试次数
// sentinelServersConfig.setRetryAttempts(3);
// // 设置重试间隔为 1.5 秒
// sentinelServersConfig.setRetryInterval(1500);
//
// // 返回 Redisson 客户端
// return Redisson.create(config);
// }
}
redisson三个重要参数
leaseTime
leaseTime
:
锁自动释放时间(或者理解为锁持有时间,持有30s和30s释放是一个意思),当出现异常 或者没手动释放锁,在到了自动释放时间时,会自动将锁释放
waitTime
-
waitTime
:
等待重试时间,有点像java原生定时器的延迟执行时间,当前线程发现锁已经被其它线程持有了(当前线程获取锁失败,通俗点解释就是 代码已经被上锁了 资源被占用了), 是应该直接重试呢 还是等待一段时间重试呢? 这种选择就是通过waitTime来控制的,当waitTime = 0时,那当前线程会马上重新尝试获取锁;如果waitTime为不为0 例如为30s, 那么当前线程会在30s再重试 尝试获取锁,如果获取成功则继续执行。由此也会延伸出一个疑问:如果waitTime时间设置过长或过短 会对锁造成什么影响吗?
答案是否定的,不会造成死锁,在一般的项目中waitTime只要不是太不合理都问题不大;
那我们严格分析一下,在高并发的项目中会发生什么:
如果waitTime过短,那它会多次尝试 造成不必要的资源损耗 ;
那如果waitTime > leaseTime (假设实际释放时间 = leaseTime )呢? 那可能等着等着已经被其它线程占用了
(虽然是lua脚本,但是只是脚本执行的时候是原子性的!比如一个脚本设置过期时间为1h 不是这1h原子性操作 而是执行脚本一瞬间是原子操作)
lockWatchdogTimeout
-
watchDog
看门狗,注意是和leaseTime互斥的!可以简单理解为:如果都强制说明了锁释放时间,那么也就不需要锁续命机制了,watchDog的默认时间是30秒,可以在配置Redisson的时候 Config里面自行修改;看门狗主要解决的是 我们可能没法很好的评估线程执行完的时间,声明时间很可能导致锁提前释放;redisson的watchDog机制 发现线程没结束 会锁续命,保证线程完全处于加锁状态中。
这里博主曾陷入一个误区,如果业务始终不完成 例如死循环 锁岂不是一直不释放 就成了死锁?
答案虽然是肯定的,但是这不是我们该考虑的范畴,业务死循环 有没有锁 都会OOM的 这不应该是我们写出来的业务代码,应该从源头去避免这个问题。
redisson常用api
-
lock()
lock是阻塞锁,没获取到锁会一直阻塞等待:线程1加锁 -> 线程2阻塞等待 -> 线程1释放锁 -> 线程2加锁 -> 线程2释放锁
-
tryLock()
tryLock是异步锁,如果发现获取锁失败 会直接返回false,我们可以手动判断加锁结果 做不同的业务 (比如false中直接return表示不等待 视为失败)以上两个api默认都是有watchDog机制的 ,因为没有显式的指定leaseTime。
-
tryLock(long waitTime, long leaseTime, TimeUnit unit)
有严谨的同学,希望完全自己掌控时间(我命由我 不由框架),会选择自己定义锁释放的时间,但是定义了leaseTime,watchDog机制就失效了。缺点:可能会和延迟双删一样的问题了,锁可能会提前释放,也可能预估不准确。
优点:保证锁在一定时间内会释放 不会阻塞。
api源码简析
leaseTime和watchDog不能共存的原因
不管是lock还是tryLock,我们不显式声明leaseTime的时候,它内部都是声明-1,
主要是在tryAcquire方法里面判断:
当配置了leaseTime,则以leaseTime为准,如果没配置 ,则会使用watchDog的时间,且会不断续命(并不是只用一次watchDog的时间就释放锁)
我们也可以用demo来验证一下:
锁没有提前释放例子
声明了leaseTime ,watchDog失效的例子
当我们声明leaseTime = 10s , 在10s就会自动将锁释放,等到我们业务代码执行30s后,再去手动释放锁会提示unlock失败 并报错: attempt to unlock lock, not locked by current thread by node id xxx
所以需要在实际业务中,我们手动释放锁要加一个判断
if (lock.isHeldByCurrentThread())
否则会抛异常
(为什么有了自动释放 还需要手动释放呢? 还是那句话 我命由我,自动的时间可能会受一些意想不到的因素影响,例如网络、jvm的gc导致的stw)
lock同步阻塞的原因
lock源码 朴实无华的同步方法
我们可以对比一下tryLock是怎么做的:
只看方法名可能不太够,我们稍微追踪一下源码(过程比较简单 就不一一贴出来了) 追踪几步后可以看到异步执行的代码: