持续创造,加速成长!这是我参与「日新方案 10 月更文应战」的第5天,点击查看活动概况
上一篇文章:用万字长文来讲讲本地锁至分布式锁的演进和Redis完结
在上一篇中挖了这个坑,今日就来把它填上哈~ ? 昨日聊到了
Redisson
完结了锁的主动续期,可是就简略提了一嘴就完事了。今日就来多说一点点里面的完结和一些运用,它和咱们自己的完结比较又怎么。
文章大纲:
榜首部分说了 Redisson
简略运用
第二部分才是说Redisson
底层源码怎么完结分布式锁
1、 怎么加锁
2、 怎么完结锁主动续期,靠什么完结的?
3、 怎么完结解锁
一、Redisson 简略运用
在 SpringBoot
中,因为主动安装的存在,运用某个封装好的轮子,就那么几步~
- 导包
- 编写装备
- 编写
xxxConfig
- 预备开端运用它
1.1、导包
Redisson
也不破例
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.6</version>
</dependency>
1.2、装备
这儿的装备便是Redis
的装备
spring:
redis:
host: IP地址
password: xxxx
1.3、编写装备类
那些什么最大衔接数,衔接超时等等就没配了,偷个懒~
@Configuration
public class MyRedissonConfig {
/**
* 一切对Redisson的运用都是经过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient() throws IOException {
//1、创立装备
Config config = new Config();
config.useSingleServer().setAddress("redis://4IP地址:6379").setPassword("xxxx");
// config.setLockWatchdogTimeout();
//2、依据Config创立出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
注意:这儿面的setAddress()
中的地址有必要以redis://
最初~
别的我这儿只是设置了这么几个属性,许多我没去写了罢了,不是它不能设置哈。
就比方修正锁主动需求的默许时刻,便是
config.setLockWatchdogTimeout();
锁主动续期默许时刻是30s,这是能够被修正的。
其他的还需靠咱们自己探索啦~
1.4、简略上手
最直接的运用:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {主启动类.class})
public class RedisTest {
@Autowired
private RedissonClient redissonClient;
@Test
public void testRedisson() {
// 获取锁实例
RLock lock = redissonClient.getLock("redisson:lock");
try {
lock.lock();
//履行需求加锁的事务~
} finally {
lock.unlock();
}
}
}
这个lock()
办法,它是能够填写参数,也能够不填的,
public void lock()
public void lock(long leaseTime, TimeUnit unit)
不填写参数,Redisson
它帮助咱们完结了锁的主动续期,
假如咱们自己填写了锁的时刻,Redisson
则不会帮咱们完结锁的主动续期。
这一点在后面剖析源码的有详细的阐明。
别的Redisson
它作为分布式锁的完结,更好的便利Java
开发者,它完结了Java
中JUC
包下的诸多接口,能够说运用起来彻底没啥学习成本~
比方JUC
下的读写锁,
@Test
public void testRedissonReadAndWriteLock() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("redisson:lock");= redissonClient.getReadWriteLock("redisson:lock");
// 获取写锁
RLock writeLock = readWriteLock.writeLock();
// 获取读锁
RLock readLock = readWriteLock.readLock();
try {
//上锁
writeLock.lock();
// 这种办法,运用咱们自己界说的
writeLock.lock(10,TimeUnit.SECONDS);
//履行需求加锁的事务~
} finally {
writeLock.unlock();
}
}
还有信号量RSemaphore semaphore = redissonClient.getSemaphore("semaphore");
更有其他的不少:
详细的案例,其实在它的文档中都有阐明的,有中文版别的,很简略的~
官方文档
这些该怎么运用,其实就和用JUC下面的工具相同,懂了那一块的知识,就知道这儿该怎么运用啦,不懂的话,我说了也还是相同的哈哈~
让我偷个懒,能够去学一学JUC,蛮好玩的~
二、锁主动续期源码-看门狗机制剖析
在剖析之前,咱们先把上一篇中分布式锁演进进程中所发生的问题都逐个抛出来,之后再针对性的看看 Redisson 它又是怎么处理的。
2.1、分布式锁演进的问题
为了便利其余没有看过看一篇文章的朋友,我把分布式锁中存在的几个问题,递推式的抽离出来了。
咱们都知道要正确运用分布式锁,一定要注意原子性操作、死锁问题和被他人开释锁的问题和锁是否需求主动续期问题。
-
死锁问题:说的是锁没有被动过期时刻,即拿到了锁,可是在履行事务进程中,程序崩溃了,锁没有被正常开释,导致其他线程无法获取到锁,从而发生死锁问题。
之前的处理办法:
set nx ex
指令 -
锁被其他人开释问题:榜首条线程抢到锁,事务履行超时,榜首条线程所持有的锁被主动开释;此刻第二条线程拿到锁,预备履行事务,刚好榜首条线程事务履行完结,照常履行了开释锁的进程,导致第二条线程持有的锁被榜首条线程所开释,锁被其他人开释。
之前的处理办法:给锁一个仅有值(UUID),每次解锁前进行判别
-
原子性操作:原子性的意思便是要么都成功,要么都失败。像咱们获取锁,设定值和设守时刻是两步操作,让他们变成原子性操作便是设定值和设守时刻成为一体,一同成功或许一同失败。别的解锁操作的获取锁,判别锁是否为当时线程所具有,也有必要是一个原子性操作。
之前的处理办法:运用
Redis
中的lua
脚本完结。 -
锁主动续期问题:这个便是运用了咱们今日的
Redisson
来完结的。
2.2、剖析的初步
咱们就以最简略的流程
@Test
public void testRedisson() {
RLock lock = redissonClient.getLock("redisson:lock");
try {
lock.lock();
//履行需求加锁的事务~
} finally {
lock.unlock();
}
}
来剖析以下几个问题:
- Redisson 是怎么完结加锁的
- Redisson 是怎么完结锁主动续期的
- Redisson 是怎么解锁的
以及在这个进程中,去看看 Redisson
它是怎么处理咱们之前呈现的问题的。
顺着看下去~
RLock lock = redissonClient.getLock("redisson:lock");
lock.lock();
看似加锁操作只调用了一个lock()
办法,但实际上流程走的可多了~
不多说,直接在Idea中点击跳转到RedissonLock
中的类中
弥补阐明:这下面还有一些完结自旋的操作,便是写了一个while(true)
来完结等候获取锁的代码,没有持续往下剖析了。中间还有一些判别锁是否能够被打断、订阅和退订等操作,没有去细心研究啦,咱们感兴趣能够再往下多看看。
接着往下走
2.3、Redisson 怎么完结加锁-tryLockInnerAsync()
漏剖析了一步,不是从scheduleExpirationRenewal(threadId)
前进到下一步,这说的是锁续期问题~
忘掉说加锁啦~ 加锁便是tryLockInnerAsync()
办法
点进去看看~
弥补阐明:
在这儿能够看到几个方面:Redisson底层也是运用Lua脚原本完结的,这段 lua 脚本分为三个部分:
-
榜首部分:加锁
首要用 exists 判别了 KEYS[1] (即
redisson:lock
)是否存在。 假如不存在,则进入第 5 行,运用hincrby
指令创立一个新的哈希表,假如域field
不存在,那么在履行指令前会被初始化为0,此指令的回来值便是履行hincrby
指令后,哈希表key
中域field
的值,此刻进行increment
,也便是回来1,之后进入第6行,对KEY[1]设置过期时刻,30000ms然后回来nil。 -
第二部分:重入
首要判别
KEY[1]
是否存在,因为KEY[1]
是一个hash
结构,所以13行意思是获取这个KEYS[1]
中字段为ARGV[2]
也便是UUID:thredId
这个值是否存在。假如存在进入14行代码对其进行加1操作(锁重入) 然后进入15行从头设置过期时刻30s 然后回来nil
-
第三部分:回来:作用便是回来 KEY[1] 的剩余存活时刻
此处的getRawName()
办法便是咱们获取到咱们设定的锁名。如此处便是获取到Redisson:lock
getLockName(threadId)
便是获取一个UUID:threadId
的字符
串。
至于说此处的UUID
,是怎么来的,我稍微浅看了一下,这是初始化的时分给每个衔接管理器都传了一个UUID的类,更详细的运用,没去追啦。
咱们知道这是 Redisson 也是采取相同的办法,它的值也是存了一个UUID,这点同样也能够在衔接工具上查看到的。
看到这儿就现已能够看出Redisson现已处理咱们昨日呈现的悉数问题了。
首要是加锁操作的原子性是满足的了,因为 Redisson
运用的Lua脚本将设置值和设置时刻的操作变为一步;其次是之前的锁被其他人开释的问题,在这儿Redisson
也选用了仅有key
值UUID
来处理此问题。
那么剩下的就只有锁续期问题了,咱们接着往下看
2.4、Redisson 怎么完结锁续期
从这个scheduleExpirationRenewal(threadId);
办法开端持续往下探索~
咱们回过头来接着看 续期办法renewExpiration()
看它底层是怎么完结的。
能够从这段代码中很明显的看出,是启动了一个守时使命,该使命每 internalLockLeaseTime/3ms
后履行一次。而 internalLockLeaseTime
默许为 30000。所以该使命每 10s 履行一次。
renewExpirationAsync(long threadId)
办法,便是完结锁从头续期的lua脚本的履行
上面的守时使命在不修正看门狗的默许时刻时,便是每10s履行一次,意思便是每次在锁还剩下20s
时,就会履行这段从头续期的代码,让锁从头续期到30s。
不知道提到这儿,大伙有听懂吗~
2.5、Redisson 怎么完结解锁操作
其实看懂了Redisson
是怎么加锁的,其实看这解锁操作也是特别简单的。
不过咱们这次要回到开端剖析的地方去啦。
看的出来,解锁操作并不复杂,咱们先去看看unlockInnerAsync(threadId);
办法吧。
其实看完加锁,再看这个其实都能理解啦吧
- 首要判别KEYS[1]是否存在
- 存在将值减1,假如counter还大于0,就从头设置过期时刻30000ms,不然就删去操作
-
redis.call('publish', KEYS[2], ARGV[1]);
同时发布了一个事件,这个是干嘛的?是去通知正在等候获取锁其他的线程们,能够运用这把锁了。
别的cancelExpirationRenewal(threadId);
办法便是一些取消和删去操作。
总结
Redisson
比较较咱们自己的完结怎么?
首要能够说的是,这个轮子考虑的比咱们周到的多,在上篇说的各种问题都是处理了的。
在各种加锁或许解锁操作上都完结了原子性~
几个点:
- 在运用
Redisson
获取锁的进程,你主动设定了锁的过期时刻,`Redisson
将不会敞开看门狗机制。 -
Redisson
在 Redis 中保存的结构是一个 Hash的数据结构,key
的称号是咱们的锁称号,如案例中运用的redisson:lock
,存储的字段称号为UUID:threadId
,值的话便是1
。 - 结构如下图:
-
Redisson
完结锁主动续期的底层是敞开了一个线程,异步的履行守时使命,在锁还剩下20s
,主动续期为30s
,此守时使命是选用Netty
结构中的时刻轮算法完结。
今日就看到这儿啦,周四挖的坑,周五熬夜总算写出来了。
跋文
不知道这篇文章有没有帮助到你,希望看完的你,关于
Redisson
现已没有那么惊骇~,当然它其实也不难。
假如觉得有收成的话,记得点点赞,给我来个关注吧~
写于 2022 年 10 月 21 日晚,作者:宁在春
今日是好累的一天啊~