Redisson是一个基于Redis的Java框架,用于实现各种分布式功能,包括分布式锁。Redisson提供了多种分布式锁的实现,其中包括可重入锁、公平锁、联锁(多个锁同时锁定或释放)、红锁(多个独立Redis节点的分布式锁),以及读写锁等。
常见问题
先说常见使用方法再深入代码讲解原理
1 Redisson配置:首先,您需要配置Redisson客户端以连接到Redis服务器。通常,这涉及创建一个Config
对象,并使用useSingleServer()
或其他方法指定Redis服务器的连接信息。示例代码中的配置是连接到本地Redis服务器的示例。
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("your word");
return Redisson.create(config)
2 使用
public class RedissonLockExample {
public static void main(String[] args) {
// 配置Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
// 创建Redisson客户端
RedissonClient redisson = Redisson.create(config);
// 获取锁
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待10秒
boolean locked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
if (locked) {
// 锁定成功,执行需要加锁的代码
System.out.println("Lock acquired, performing the critical section");
Thread.sleep(5000); // 模拟锁定后的操作
} else {
// 锁定失败
System.out.println("Failed to acquire the lock");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
System.out.println("Lock released");
}
// 关闭Redisson客户端
redisson.shutdown();
}
}
原理讲解
重入锁原理
重入锁(Reentrant Lock)是一种高级的同步工具,它允许同一个线程多次获取同一把锁,而不会发生死锁。这意味着一个线程在持有锁的情况下可以多次进入锁保护的代码块,而不会被自己阻塞
-
锁计数器:重入锁内部维护一个锁计数器,用于跟踪锁的持有次数。初始时,锁计数器为0,表示没有线程持有该锁。
-
加锁操作:当一个线程首次请求加锁时,锁计数器会增加,同时记录下持有锁的线程。此时,线程获得了锁,并且可以执行锁保护的代码块。
-
重入:如果同一个线程再次请求加锁(重复加锁),锁计数器会继续增加,表示锁被持有多次。线程在退出锁保护的代码块之前,可以多次加锁和解锁,而锁计数器会相应地增加和减少。
-
解锁操作:每次线程解锁时,锁计数器减少。只有当锁计数器减少为0时,锁才会被完全释放,其他线程才有机会获得锁。
作用:
-
避免死锁:重入锁允许同一线程多次获取锁,因此不会因为线程自己持有的锁而导致死锁。这在复杂的多线程场景中非常有用,因为线程可能需要在执行一些递归函数或者多层嵌套的方法时多次获取锁。
-
精细控制锁的释放:与传统的synchronized关键字相比,重入锁允许更灵活地控制锁的释放。线程可以在锁保护的代码块内多次获取和释放锁,而不必将整个代码块包裹在同一个synchronized块中。
打开trylock源码:
(这里通过redis的hash结构来实现锁的重入,如果第一次获取锁就创建,并把value设置为1,再次有线程想要获取锁就再次增加value的值,释放锁时每当一个线程释放时value就-1.知道为0彻底释放完成)
调用了tryLockAsync方法
由于初始时未填写过期时间等待时间等信息,默认为-1,进而再次调用tryAcquireOnceAsync方法
在这里call方法内部是lua脚本实现操作的原子性
当==0时表示之前未有线程获取锁创建并赋值。当==1时表示存在,为了实现重入就在value上加一,并设置过期时间。注意 这里返回nil代表成功,失败返回对应的时间毫秒值pttl
释放锁
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0;
else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;",
Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
}
每次释放锁都会对数量减一直至0,并且发布释放锁的通知
重试获取锁机制讲解
trylock源码
这里默认ttl为-1
注意tryAcquire方法,返回值为ttl,ttl为null即为获得锁成功
这里由于默认存活时间为-1,所以下面参数默认存活时间为
this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()
(watchdog看门狗)也就是30s。
查看trylockInnerAsync方法
和上面的代码一样接下来回到trylock源码
如果ttl为null那么返回true代表获取成功
否则用最大等待时间time减去上面的系统时间算出这段代码的耗时,如果为负数说明超过最大等待时长,返回false,如果time大于0,不直接判断,因为此时别的线程获取锁正在执行,假设立马执行只是会浪费cpu资源,所以这里用了subscribe(threadId)方法来订阅锁释放的信息(上面的unlock代码释放锁会发布信息),然后采用计数器进行等待,等待时长为time,假设没等到,返回false,那么使用unsubscribe()方法结束订阅,返回false。
如果等到别的线程释放锁,就再次判断上面代码是否超时,超时返回false,否则再次带哦用tryAcquire方法
如果ttl小于等待时间time,那么就尝试ttl时间,否则就尝试获取锁在time时间内,
知道time结束,这就时重试获取锁机制
Redisson锁超时解决原理
如果业务堵塞导致过期,并未主动释放锁,从而导致其他线程获取锁,这时容易产生安全问题,一起看看redisson如何解决这个问题
在tryAcquireAsync方法中,有方法onComplete方法,,会调用scheduleExpirationRenewal(threadId)方法时间表刷新方法。
这里创建一个ExpirationEntry类
Redisson 中的 ExpirationEntry 是 Redisson 框架中用于管理 Redis 数据过期的一个内部数据结构,通常不是公共 API 的一部分。ExpirationEntry主要用于实现 Redisson 提供的分布式对象(例如分布式 Map、分布式集合等)的过期功能。这个类不是 Redis 自身的概念,而是 Redisson 框架为了实现某些功能而引入的内部结构。
具体来说,ExpirationEntry用于跟踪 Redisson 中的某个分布式对象的过期时间,以便在数据过期时能够自动清理或触发相应的操作。这对于缓存、锁或其他需要管理过期数据的分布式场景非常有用。
这个类的具体实现和使用方式通常是 Redisson 框架内部的细节,一般不需要直接操作它。Redisson 提供了高级的分布式数据结构和工具,开发人员可以直接使用这些工具来构建分布式应用程序,而不必关心底层的细节。
然后执行了puIfAbsent方法,这里的EntryName是当前连接id+创建RLock时传入的锁的名称
第一次执行会进入renewExpiration方法
这是一个延迟执行的方法,延迟时间为
this.internalLockLeaseTime / 3L,如果开始未传入ttl时间那么默认internalLockLeaseTime为看门狗时间30s,如果传入的有ttl,以传入的为准。
这个任务执行的是
RedissonLock.this.renewExpiration();
等于说是每过ttl时间的三分之一就执行这个任务一次相当于不会出现因为业务代码延迟导致锁的ttl时间过期,那么怎么释放呢
看unlock
进入unlockAsync方法
如果有异常就是e!=null。进入cancelExpirationRenewal方法
这里就解决了定时刷新的问题
也就是说如果不设置存活时间,那么会利用看门狗执行任务刷新等待
释放锁逻辑图