Redis实战-基于redis和lua脚本实现分布式锁以及Redission源码解析【万字长文】
前言:
在上篇博客中,我们探讨了单机模式下如何通过悲观锁(synchronized)实现"一人一单"功能。然而,在分布式系统或集群环境下,单纯依赖JVM级别的锁机制会出现线程并发安全问题,因为这些锁无法跨JVM生效。
(详情请参考我的上一篇博客:Redis实战-单机项目下实现优惠劵秒杀【万字长文】)
那么,在分布式环境下如何正确实现"一人一单"功能呢?本文将深入探讨分布式锁的实现方案,重点介绍基于Redis和Lua脚本的分布式锁实现,并分析Redission框架的源码实现。
今日所学:
- 分布式锁原理及其实现
- redis分布式可能造成的误删问题
- 引入lua脚本解决多条命令的原子性问题
- Redisson源码解析
1.分布式锁原理及其实现
1.1 什么是分布式锁
分布式锁是一种在分布式系统中用于协调多个节点或进程对共享资源进行互斥访问的机制。它的核心目标是确保在分布式环境下,同一时间只有一个节点能够执行关键操作(如修改共享数据、访问数据库等),避免并发导致的数据不一致问题。
一句话总结:
分布式锁是在分布式系统中协调多个节点对共享资源访问的一种同步机制。
同俗一点解释就是,synchronized
只能管住自己JVM里的线程,而Redis分布式锁相当于一个"全局管理员",能管住所有JVM的线程,让它们在集群里排队用资源。就好比单机游戏(只能在本机内存档)和网游游戏的区别(所有电脑都可以读取存档)
分布式锁的特性有:
- 互斥性:同一时刻只能有一个节点持有锁。
- 可重入性:同一个节点多次请求锁时能够成功(避免死锁)。
- 超时释放:锁需设置超时时间,防止节点崩溃后锁无法释放。
- 高可用性:锁服务需具备容错能力,避免单点故障。
- 高性能:获取和释放锁的操作应高效。
1.2 常见的分布式锁
常见的分布式锁有三种
Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案,由于黑马的那套视频并不讲解zookeeper的原理和分布式锁的实现,所以不过多阐述
这里我们使用redis作为分布式锁。
1.3 redis分布式锁的实现
1.3.1 核心思路:
分布式锁应该说但凡是锁都要实现的两个基本方法:
1.获取锁:
- 互斥:确保只能有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
2. 释放锁:
- 手动释放
- 超时释放:获取锁时添加一个超时时间
那么我们改如何去实现呢,redis中有什么指令或者说方法可以满足这两点呢?
我们先说获取锁,核心思路是我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可
想通了这个,释放锁就非常简单了,要么等业务执行完自动删除,要么就是服务宕机或者业务执行时间过长到达锁的超时时间,锁自动删除。
最后问题就变成了如何设置一个指令(只能一条指令以保证原子性),既能实现setnx保证锁的互斥性,同时增加过期时间,防止死锁。
1.3.2 代码实现:
1.获取锁(util包下SimpleRedisLock类)
逻辑思路很简单:
1.先获取线程标识
2.获取锁(等同于redis中的SET lock_key unique_value NX PX 30000,创建一个redis字段)nx等同于setnx代表互斥,ex设置过期时间,一条指令保证了原子性。
3.根据返回的boolean确定是否获取锁成功(成功返回1,失败返回0),这边使用Boolean.TRUE是为了防止Boolean包装类有返回null的情况
private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
2.释放锁(utils下SimpleRedisLock类):
要释放锁时给相应的redis锁删除就行
public void unlock() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
最后在service层下的VoucherOrderServiceImpl类下修改相应的代码:
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
Long userId = UserHolder.getUser().getId();
//创建锁对象(新增代码)
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
2.redis分布式锁可能造成的误删问题
2.1 问题分析
我们假设持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明
2.2 解决思路
解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁
2.3 需求分析
修改之前的分布式锁实现,满足:
在获取锁时存入线程标示(可以用UUID表示,在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
-
如果一致则释放锁
-
如果不一致则不释放锁
核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除
2.4 代码实现
在util包下SimpleRedisLock类中,对代码进行修改
具体修改逻辑:
1.生成一个静态常量UUID随机数
2.在加锁操作中,将生成的随机数与进程编号拼接,得到一个唯一的进程ID(防止多进程下编号相重)
2.在释放锁那,从锁中获取value值,并跟线程标识进行比较,如果相同。则是自己的锁,可以删除,如果不同,则不能删除
加锁
```java
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
```
释放锁
```java
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
3. 引入lua脚本解决多条命令的原子性问题
3.1 极端情况说明
线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,也就是确定这把锁就是自己的了。但是此时线程1出现了阻塞,锁超时,线程2获得锁,执行业务,就在此时,线程1停止阻塞,执行删除操作(因为已经走过了判断过程),线程2的锁被删除。线程3获得锁和线程2并发执行。而导致这的原因是因为判断和执行删除是两个操作,没能实现操作的原子性。
3.2 lua脚本解决原子性问题思路
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了。
3.3 lua语言redis调用
这里重点讲下如何在lua语言中调用redis,语法如下:
```lua
redis.call('命令名称', 'key', '其它参数', ...)
```
例如,我们要在lua语言中执行set name jack,脚本是这样的
redis,call('set', 'name', 'jack')
在比如,我们要先执行set name Rose, 再执行 get name,则脚本如下:
# 先执行set name Rose
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
注意这里lua是一门弱语言(跟某py一样),所以没有什么变量类型只说,全局变量就加个local
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
EVAL script numkeys [keys] arg[arg...]
比如说要调用set name jack 这个脚本
eval 'return redis.call('set', 'name', 'jack')' 0
这个的0代表这传入了0个KEYS参数,'name'和‘jack’作为普通字符传入(就相当于传入常量,不设变量)
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数
EVAL"return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
其中1代表着传入的KEYS的参数的数量,比如这条语句因为传入的参数是1,所以KEYS[1]数组长度为1,name作为key,其他的都作为ARGV参数(当然,这里只传入了一个Rose,所以ARGV[1]长度也为1)
3.4 代码实现
接下来我们来回顾一下我们释放锁的逻辑:
释放锁的业务流程是这样的
- 获取锁中的线程标示
- 判断是否与指定的标示(当前线程标示)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
如果用Lua脚本来表示则是这样的:
1.获取Java中传入的key和线程标识
2. 通过key从redis锁中获取线程标识value
3.比较,如果相同,则删除,不然返回0表示false
-- 锁的key local key = KEYS[1]-- 当前线程标识 local threadId = ARGV[1]-- 获取锁中的线程标识 get key local id = redis.call('get', key)-- 比较线程标示与锁中的标识是否一致 if(id == ARGV[1]) then-- 释放锁 del keyreturn redis.call('del', KEYS[1]) end return 0
lua脚本写好了,那么怎么在Java中调用呢
我们的RedisTemplate中,可以利用execute方法去执行lua脚本,参数对应关系就如下图
具体代码如下(大致看懂什么意思就行):
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
这里可以看到,经过我们的改造,原本几行代码被我们改为1行,使用lua脚本,我们就能够实现 拿锁和锁删锁的原子性动作了。
3.5 总结
基于Redis的分布式锁实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
做到这,我们先是setnx, ex(防止死锁)实现了一个简单的分布式锁,然后为了解决误删问题,我们又引入了线程标识作为评判标准,但是只是在Java代码中加入if()判断的话,仍可能出现线程安全问题(不是原子性的),因此我们又引入了lua脚本来解决这个问题。
但是问题来了,引入ex设置过期时间是解决了死锁问题,但是也造成了另一个问题:时间不好把握,如果业务执行时间过长,还有执行完所就给释放了怎么办,这就是所谓的锁不住问题,为了解决这个问题,我们需要一种机制,当过期时间到了但是业务没有执行完,我们可以刷新下它的过期时间,相当于给它续费。
4. Redission框架介绍
4.1 setnx锁存在的问题
我们说下使用setnx使用分布式锁存在的几个问题:
1.重入问题
重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的
2.不可重试
是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
3. 超时释放
我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患。
4. 主从一致性
如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。
那么如何解决这些问题呢?下面我们就来介绍一个框架Redission
4.2 Redission介绍
什么是Redission呢?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
一句话总结:
Redisson 是一个基于 Redis 的高级 Java 分布式服务框架,提供分布式锁、数据结构、远程服务等企业级功能,大幅简化分布式系统开发。
Redission提供了分布式锁的多种多样的功能。
官网地址:https://redission.org
Github地址:https://github.com/redission/redission
4.3 Redission快速入门
1. 在pom.xml引入相应依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.在config包下配置相应的Redission客户端
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6379")
.setPassword("123321");
// 创建RedissonClient对象
return Redisson.create(config);
}
}
3.进行测试(test包下新建一个RedissionTest类)
1.创建锁,指定锁的名称
2.尝试获取锁,传入最大等待时间(用于重试),锁的自动释放时间,时间单位
3. 判断是否获取锁成功
4。业务执行完释放锁
@Resource
private RedissionClient redissonClient;
@Test
void testRedisson() throws Exception{
//创建锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
4. 测试成功,更改service包下VoucherOrderServiceImpl类相应的逻辑
主要更改有两点:
1.依赖注入,注入相应的redissionClient类
2.给自定义的simpleRedisLock类创建锁对象改成使用redissionClient自带的内置方法创建锁对象
@Resource private RedissonClient redissonClient;/*** 抢购优惠卷* @param voucherId* @return*/@Override public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 尚未开始return Result.fail("秒杀尚未开始");}// 3.判断秒杀是否结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock() < 1){// 库存不足return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();// 创建锁对象//SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId); RLock simpleRedisLock = redissonClient.getLock("lock:order:" + userId);// 获取锁boolean isLock = simpleRedisLock.tryLock();// 判断是否获取锁成功if(!isLock){// 获取锁失败, 返回错误或者重试return Result.fail("一个人只能下一单");}try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} catch (IllegalStateException e) {e.printStackTrace();} finally {simpleRedisLock.unlock();}return null; }
5. Redission源码解析
首先,我们回顾下setnx存在的几个问题,并逐一对这几个问题进行解决。
5.1 hash结构解决不可重入问题
通过设置一个hash结构,让filed储存线程标识,value储存重入次数。
利用唯一Key保证互斥性(不同线程竞争时只有一个能成功创建锁,exists判断),同时通过Field记录线程ID+Value计数实现可重入(同一线程多次获取锁时计数器+1)。底层使用Lua脚本保证「判断存在→设置锁→设置过期时间」的原子性操作,既防止并发冲突,又支持锁的重入,最终通过「计数器归零删除Key」来安全释放锁。
-- 公共变量 local key = KEYS[1] local threadId = ARGV[1] local releaseTime = ARGV[2]-- 取锁 -- 判断是否存在 if (redis.call('exists', key) == 0) then-- 不存在,获取锁redis.call('hset', key, threadId, '1');-- 设置有效期redis.call('expire', key, releaseTime);return 1; -- 返回结果 end-- 锁已经存在,判断threadId是否是自己的 if (redis.call('hexists', key, threadId) == 1) then-- 获取锁 重入次数+1redis.call('hincrby', key, threadId, '1');--设置有效期redis.call('expire', key, releaseTime);return 1; endreturn 0;-- 解锁
-- 判断当前锁是否还是被自己锁持有 if (redis.call('HINCRBY', key, threadId) == 0) thenreturn nil; -- 如果不是自己的,直接返回 end-- 是自己的锁,则重入次数-1 local count = redis.call('HINCRBY', key, threadId, -1);-- 判断是否冲入次数已经为零 if (count > 0) then-- 大于0说明不能释放锁redis.call('expire', key, releaseTime);return nil; elseredis.call('del', key);return nil; end
接下来我们打开Redission源码(RLock下实现类RedissionLock tryLockInnerAsync方法)
在tryLockInnerAsync方法下可以看到如出一辙的加锁模式:
1.判断锁是否存在,不存在创建锁
2.存在并且是同一线程则value值加一,并刷新过期时间
3.如果锁存在并且不是为当前线程所持有(没有进行if语句),则返回当前锁的剩余时间,也就是锁到什么时候过期,以此后续确定是否重试。
5.2 信号量和Pubsub机制解决不可重试问题
不可重试问题,主要指的是锁竞争失败后无法自动重试。在Redission框架中,这主要依靠异步等待+订阅发布机制解决
这里先介绍下什么是异步等待:
异步等待是一种非阻塞的资源竞争处理机制,其核心特点是:当资源不可立即获取时,不会阻塞当前线程,而是通过事件监听或回调机制,在资源可用时自动恢复执行。这种模式在分布式系统和高并发场景中至关重要。
比如在解决不可重试问题时,使用异步等待,如果不成功,不会陷入阻塞,而是取执行查看订阅信息频道,以此进行超时判断或者重试
1.回到Redission的源码,找到tryAcquireAsync方法
leaseTime代表着等待超时时间,如果设置了等待超时时间则用自己的超时时间,不然启用看门狗机制自动续约(这个后面会讲),我们现在主要就重试问题进行分析。
在重入问题中,我们讲过如果获取到了锁,就返回null,没有获取到锁,返回当前线程的剩余时间
这个方法中返回的结果记录为ttlRemainingFuture
如果为ttl为null的话记录等待超时时间或启用看门狗机制,否则继续返回剩余时间。
下面是完整代码:
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// lock acquiredif (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f); }
2.ttl值通过get()同步传递到tryAcquire方法,trylock方法通过调用tryAcquire执行重试机制。
这边注意不是lock方法是trylock方法。
接下来我们来到trylock方法,传入等待重试时间(waittime),等待超时时间(锁持有时间leaseTime),还有时间单位。
ttl(当前锁的剩余时间,也就是锁到什么时候过期)时间由tryAcquire方法获取,底层逻辑就是获取锁的那段lua脚本
如果ttl为null的话,表示成功获取到锁,直接返回
否则看是否超时,执行尝试获取锁的线程后的时间(system.current)减去执行前的时间(current/waitime),如果小于零,表示超过等待重试时间,不再重试,直接返回
如果没有拿到锁并且还剩下时间,则向redis发送订阅消息,监听该锁的释放事件,并等待订阅完成(让线程进入阻塞等待状态)
做完后在检测waitTime剩余时间,如果小于零,不在重试,直接返回
否则进入while循环,做4件事
1.尝试获取锁
2.查看waitTime剩余时间,判断是否超时
3.确定最长阻塞等待时间,这里的if对应两种情况
- ttl(锁的剩余时间)在waitTime剩余时间之内,阻塞等待最长时间设置为ttl
- 如果不在,则阻塞等待最长时间设置为waitTime剩余时间(超出时间取消订阅)
然后执行阻塞等待,期间可能被两种事件唤醒:
- 超时通知(到达时间超过ttl或者time的时间限制)
- 锁释放通知(持有锁线程通过Pubsub(订阅信息通道)发送信息)
4.判断剩余时间
最后while循环结束不管没有没拿到锁,都要去取消订阅
最后流程图:
总结下:
Redission利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
5.3 Watchdog机制解决超时释放问题
什么是WatchDog机制?
Redisson的看门狗机制(Watchdog)是一种用于自动续期分布式锁的机制,主要解决客户端在持有锁期间因业务执行时间过长导致锁超时释放的问题。该机制通过后台线程定期检查并延长锁的持有时间,确保锁的安全性。
一句话总结,Watchdog机制通过一种定时续约逻辑来解决分布式锁过期的问题。
来到Redission源码,一样是在RedissionLock类中,回到tryAcquireOnceAsync方法,之前我们在讲解决不可重试问题时讲过,锁的等待超时(也就是过期时间leaseTime)可以设可以不设,如果设置的话,就使用自己设置的过期时间,如果没有设置过期时间,就走看门狗(watchdog)机制。
我们看到如果没有设置leaseTime,源码传达的时nternalLockLeaseTime,它的初始化是这样的:
进入getLockWatchdogTimeout()方法
发现return 了一个lockWatchdogTimeout,这个一个long类型的变量,值为30 * 1000
由此我们知道,采用看门狗机制它的初始锁的超时释放时间设置的是30s,那么他是如何做到自动续时的呢,我们往后看。
可以看到,红框部分同样有个判断,如果设置了leaseTime,则将其转换成毫秒单元。如果没有,则走scheduleExpirationRenewal()方法
进入scheduleExpirationRenewal()方法
当线程成功获取锁后,会调用此方法,执行以下逻辑:
1.尝试为当前锁新建一个续期条目,将当前线程ID加入其中
2.判断该锁是否已经有续期条目(确保同一个锁的多个线程共享同一个续期任务,防止重复续期)
3.如果已经存在,只需要将当前线程ID添加到该锁现有条目中
4.如果是新条目,执行续期任务(renewExpiration())
5.最后,如果线程被中断,取消续期
换言之,对于这个方法,他主要做两件事:
- 对已有续期条目的锁,给新的线程ID添加进去(续期任务已开启,满足相应条件自动续期)
- 对没有续期条目的锁,创建新的续期条目,开启续期任务
那么续期任务是具体怎么执行的呢?我们打开rennewExpiration()方法
1.获取当前的续期条目,如果没有,直接返回(代表着锁被释放或者未初始化)
2.创建一个定时任务
其中internalLockLeaseTime默认为30s,也就是每10s默认执行一次定时任务
3.进入到定时任务的具体逻辑中,二次检查是否存在相应的续约条目和线程
4.调用renewExpirationAsync方法,更新锁的过期时间,执行续约,返回一个boolean值
下面是renewExpirationAsync具体逻辑:
可以看到执行lua脚本实现过期时间的更新。
5.先是进行异常判断,然后根据传递的boolean值(lua脚本的执行结果)判断是否进行递归(继续每10s进行续约)还是取消续约任务。
具体执行流程:
5.4 multiLock解决主从一致性问题
原有锁存在问题:
redis集群主节点获得锁后可能立即宕机,没有及时给数据同步从节点
解决方法:
设立多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
这样做,此时就算假设此时有一个主节点宕机,其他线程趁虚而入获得那个节点的锁,只要没有获得其他所有主节点的锁,也是获取失败的。
最后:
今天的分享就到这里。如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)
相关文章:

Redis实战-基于redis和lua脚本实现分布式锁以及Redission源码解析【万字长文】
前言: 在上篇博客中,我们探讨了单机模式下如何通过悲观锁(synchronized)实现"一人一单"功能。然而,在分布式系统或集群环境下,单纯依赖JVM级别的锁机制会出现线程并发安全问题,因为这…...
Ubuntu崩溃修复方案
当Ubuntu系统崩溃时,可依据崩溃类型(启动失败、运行时崩溃、完全无响应)选择以下修复方案。以下方法综合了官方推荐和社区实践,按操作风险由低到高排序: 一、恢复模式(Recovery Mode) 适用场景:系统启动卡顿、登录后黑屏、软件包损坏等。 操作步骤: …...

计算机网络 : 应用层自定义协议与序列化
计算机网络 : 应用层自定义协议与序列化 目录 计算机网络 : 应用层自定义协议与序列化引言1. 应用层协议1.1 再谈协议1.2 网络版计算器1.3 序列化与反序列化 2. 重新理解全双工3. socket和协议的封装4. 关于流失数据的处理5. Jsoncpp5.1 特性5.2 安装5.3…...

Python Day42 学习(日志Day9复习)
补充:关于“箱线图”的阅读 以下图为例 浙大疏锦行 箱线图的基本组成 箱体(Box):中间的矩形,表示数据的中间50%(从下四分位数Q1到上四分位数Q3)。中位线(Median)&#…...

CMake在VS中使用远程调试
选中CMakeLists.txt, 右键-添加调试配置-选中"C\C远程windows调试" 之后将 aunch.vs.json文件改为如下所示: CMake在VS中使用远程调试时,Launch.vs.json中远程调试设置 ,远程电脑开启VS专用的RemoteDebugger {"version": "0.2.1","defaul…...

《图解技术体系》How Redis Architecture Evolves?
Redis架构的演进经历了多个关键阶段,从最初的内存数据库发展为支持分布式、多模型和持久化的高性能系统。以下为具体演进路径: 单线程模型与基础数据结构 Redis最初采用单线程架构,利用高效的I/O多路复用(如epoll)处…...
从零搭建到 App Store 上架:跨平台开发者使用 Appuploader与其他工具的实战经验
对于很多独立开发者或小型团队来说,开发一个 iOS 应用并不难,真正的挑战在于最后一步:将应用成功上架到 App Store。尤其是当你主要在 Windows 或 Linux 系统上开发,缺乏苹果设备和 macOS 环境时,上架流程往往变得繁琐…...
Spring Cloud 2025 正式发布啦
文章目录 一、版本兼容性二、Spring Cloud Gateway 重大更新1、新增功能1.1 Function & Stream 处理器集成1.2 Bucket4j 限流器支持 2、重要弃用2.1. WebClientRouting 基础设施2.2. 模块和启动器重命名 3、破坏性变更3.1 X-Forwarded-* 头部默认禁用3.2 配置受信任代理:3.…...

一文速通Python并行计算:12 Python多进程编程-进程池Pool
一文速通 Python 并行计算:12 Python 多进程编程-进程池 Pool 摘要: 在Python多进程编程中,Pool类用于创建进程池,可并行执行多个任务。通过map、apply等方法,将函数和参数分发到子进程,提高CPU利用率&…...
相机Camera日志分析之二十五:高通相机Camx 基于预览1帧的process_capture_request四级日志分析详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:相机Camera日志分析之二十四:高通相机Camx 基于预览1帧的process_capture_request三级日志分析详解 ok 这一篇我们开始讲: 相机Camera日志分析之二十五:高通相机Camx 基于预览1帧的process_capture_…...
React从基础入门到高级实战:React 实战项目 - 项目一:在线待办事项应用
React 实战项目:在线待办事项应用 欢迎来到本 React 开发教程专栏的第 26 篇!在之前的 25 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件、状态、路由和性能优化等核心知识。这一次,我们将通过一个…...
云部署实战:基于AWS EC2/Aliyun ECS与GitHub Actions的CI/CD全流程指南
在当今快速迭代的软件开发环境中,云部署与持续集成/持续交付(CI/CD)已成为现代开发团队的标配。本文将详细介绍如何利用AWS EC2或阿里云ECS结合GitHub Actions构建高效的CI/CD流水线,从零开始实现自动化部署的全过程。 最近挖到一个宝藏级人工智能学习网…...
golang 如何定义一种能够与自身类型值进行比较的Interface
定义一种具有比较能力的类型是一种常见需求,比如对一组相同类型的值进行排序,就需要进行两两比较,那么在Go语言中有没有办法定义一种具有比较能力的Interface,实现该接口的类型都具备比较能力呢,最常见最容易的办法是定…...

Web前端之原生表格动态复杂合并行、Vue
MENU 效果公共数据纯原生StyleJavaScript vue原生table 效果 原生的JavaScript原生table null 公共数据 const list [{id: "a1",title: "第一列",list: [{id: "a11",parentId: "a1",title: "第二列",list: [{ id: "…...

『uniapp』把接口的内容下载为txt本地保存 / 读取本地保存的txt文件内容(详细图文注释)
目录 预览效果思路分析downloadTxt 方法readTxt 方法 完整代码总结 欢迎关注 『uniapp』 专栏,持续更新中 欢迎关注 『uniapp』 专栏,持续更新中 预览效果 思路分析 downloadTxt 方法 该方法主要完成两个任务: 下载 txt 文件:通…...
C/C++ 面试复习笔记(2)
C语言如何实现快速排序算法? 答案:快排是一种分治算法,选择一个基准元素,将数据划分成两部分,然后递归排序 补充: void quick_sort(int arr[], int start, int end) {//判断是否需要排序if (start > …...
宝马集团推进数字化转型:强化生产物流与财务流程,全面引入SAP现代架构
2025年6月,宝马集团宣布在生产物流与财务流程领域取得重大数字化成果。这些进展标志着集团全球范围内采用基于云的新型SAP架构进入关键阶段,旨在提升运营效率、透明度和AI能力,为未来工业发展奠定技术基础。 一、生产物流全球数字化部署 宝…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 时间事件处理部分)
揭秘高效存储模型与数据结构底层实现 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 时间事件:serverCron函数更新服务器时间缓存更新LRU时钟-lruclock更新服务器每秒执行命令次…...

【DAY40】训练和测试的规范写法
内容来自浙大疏锦行python打卡训练营 浙大疏锦行 知识点: 彩色和灰度图片测试和训练的规范写法:封装在函数中展平操作:除第一个维度batchsize外全部展平dropout操作:训练阶段随机丢弃神经元,测试阶段eval模式关闭drop…...
C语言 标准I/O函数全面指南
C标准I/O函数全面指南 本指南详细介绍了C语言中用于文件操作的标准输入/输出函数,包括单字符I/O、字符串I/O、格式化I/O、块I/O以及文件光标操作。每个部分包含函数定义、使用说明和实用示例,适合学习、复习以及博客发布。内容采用清晰的Markdown格式&a…...

el-select 实现分页加载,切换也数滚回到顶部,自定义高度
el-select 实现分页加载,切换也数滚回到顶部,自定义高度 1.html <el-form-item label"俱乐部:" prop"club_id" label-width"120px"><el-select :disabled"Boolean(match_id)" style"w…...

Langchaine4j 流式输出 (6)
Langchaine4j 流式输出 大模型的流式输出是指大模型在生成文本或其他类型的数据时,不是等到整个生成过程完成后再一次性 返回所有内容,而是生成一部分就立即发送一部分给用户或下游系统,以逐步、逐块的方式返回结果。 这样,用户…...
Jenkins:自动化流水线的基石,开启 DevOps 新时代
从持续集成到持续交付的全流程自动化工具 一、什么是 Jenkins? Jenkins 是一款开源的 自动化服务器,专注于持续集成(CI)和持续交付(CD)。它通过插件化的架构支持几乎所有的开发、运维和测试工具ÿ…...

学习经验分享【40】目标检测热力图制作
目标检测热力图在学术论文(尤其是计算机视觉、深度学习领域)中是重要的可视化分析工具和论证辅助手段,可以给论文加分不少。主要作用一是增强论文的可解释性与说服力:论文中常需解释模型 “如何” 或 “为何” 检测到目标…...

C#里与嵌入式系统W5500网络通讯(3)
有与W5500通讯时,需要使用下面的寄存器: PHYCFGR (W5500 PHY Configuration Register) [R/W] [0x002E] [0b10111XXX] PHYCFGR configures PHY operation mode and resets PHY. In addition, PHYCFGR indicates the status of PHY such as duplex, Speed, Link. 这张表格详细…...

用OpenNI2获取奥比中光Astra Pro输出的深度图(win,linux arm64 x64平台)
搞了一个奥比中光Astra Pro,想在windows平台,和linux rk3588 (香橙派,ubuntu2404,debian)上获取深度信息,之前的驱动下载已经不好用了,参考如下 Astra 3D相机选型建议 - 知乎https://zhuanlan.zhihu.com/p/594485674 …...

Unity VR/MR开发-VR设备与适用场景分析
视频讲解链接:【XR马斯维】VR/MR设备与适用场景分析?【UnityVR/MR开发教程--入门】_游戏热门视频...

Linux: network: switch:arp cache更新规则 [chatGPT]
文章目录 介绍概念普通包带有不同的mac,是否更新arp cache?普通包带有相同的mac,是否刷新 aging timeswitch是否会主动学习介绍 关于arp cache在switch侧的行为。有很多问题需要理解。 概念 HP L3 - IP Services Configuration Guide 文档里有写:dynamic arp entry的解说…...

Java网络编程API 1
Java中的网络编程API一共有两套:一套是UDP协议使用的API;另一套是TCP协议使用的API。这篇文章我们先来介绍UDP版本的API,并尝试来写一个回显服务器(接收到的请求是什么,返回的响应就是什么)。 UDP数据报套…...
Android协程学习
目录 Android上的Kotlin协程介绍基本概念与简单使用示例协程的高级用法 结构化并发线程调度器(Dispatchers)自定义调度器并发:同步 vs 异步 异步并发(async 并行执行)同步顺序执行协程取消与超时 取消机制超时控制异步数据流 Flow协程间通信 使用 Channel使用 StateFlow /…...