lock.lock(30, TimeUnit.SECONDS); // 尝试获取锁30秒,如果获取不到则放弃
//尝试获取锁,等待5秒,持有锁10秒钟
boolean success = lock.tryLock(0, 10, TimeUnit.SECONDS);
Redisson 是一种基于 Redis 的分布式锁框架,提供了 lock()
和 tryLock()
两种获取锁的方法。
lock()
方法是阻塞获取锁的方式,如果当前锁被其他线程持有,则当前线程会一直阻塞等待获取锁,直到获取到锁或者发生超时或中断等情况才会结束等待。该方法获取到锁之后可以保证线程对共享资源的访问是互斥的,适用于需要确保共享资源只能被一个线程访问的场景。Redisson 的 lock()
方法支持可重入锁和公平锁等特性,可以更好地满足多线程并发访问的需求。
而 tryLock()
方法是一种非阻塞获取锁的方式,在尝试获取锁时不会阻塞当前线程,而是立即返回获取锁的结果,如果获取成功则返回 true,否则返回 false。Redisson 的 tryLock()
方法支持加锁时间限制、等待时间限制以及可重入等特性,可以更好地控制获取锁的过程和等待时间,避免程序出现长时间无法响应等问题。
因此,两种获取锁的方式各有优缺点,在实际应用中需要根据具体场景和业务需求来选择合适的方法,以确保程序的正确性和高效性。
直接看代码例子lock.tryLock等待时间和持有时间都为0时。
public String RedissonLock1() {
RLock lock = redissonClient.getLock("order_lock");
boolean success = true;
try {
System.out.println("获取锁前的时间:"+LocalDateTime.now());
// 尝试获取锁,等待5秒,持有锁10秒钟
success = lock.tryLock(0, 0, TimeUnit.SECONDS);
// lock.lock(0, TimeUnit.SECONDS);
System.out.println("获取锁后的时间:"+LocalDateTime.now());
if (success) {
System.out.println(Thread.currentThread().getName() + "获取到锁"+ LocalDateTime.now());
// 模拟业务处理耗时
// TimeUnit.SECONDS.sleep(3);
// 模拟业务处理耗时 大于锁过期,可能导致非自己持有的锁被释放。
TimeUnit.SECONDS.sleep(20);
} else {
System.out.println(Thread.currentThread().getName() + "未能获取到锁,已放弃尝试");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 判断当前线程是否持有锁
if (success && lock.isHeldByCurrentThread()) {
//释放当前锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放锁"+ LocalDateTime.now());
}
}
return "";
}
可以看到立即拿锁,拿不到立即就返回
lock()方法的持有锁时间为0时
public String RedissonLock2() {
RLock lock = redissonClient.getLock("order_lock");
boolean success = true;
try {
System.out.println("获取锁前的时间:"+LocalDateTime.now());
// 尝试获取锁,等待5秒,持有锁10秒钟
// success = lock.tryLock(0, 0, TimeUnit.SECONDS);
lock.lock(0, TimeUnit.SECONDS);
System.out.println("获取锁后的时间:"+LocalDateTime.now());
// 模拟业务处理耗时
// TimeUnit.SECONDS.sleep(3);
// 模拟业务处理耗时 大于锁过期,可能导致非自己持有的锁被释放。
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 判断当前线程是否持有锁
if (success && lock.isHeldByCurrentThread()) {
//释放当前锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放锁"+ LocalDateTime.now());
}
}
return "";
}
可以看到第二次获取锁时是阻塞的,等着业务处理了20s后,才获取到锁。
lock()
方法是阻塞获取锁的方式,而 tryLock()
方法是一种非阻塞获取锁的方式。
可以结合文章一起看 看懂Redisson分布式锁源码,其实并不难
看门狗续约时间默认是30秒
lock()
和 tryLock()都会用的
tryAcquireAsync(),看到区别,其实核心是leaseTime是否 > 0
所以是否使用看门狗不算是tryLock和lock()的区别,可以看下面这5种情况。
//走if (leaseTime > 0)逻辑
lock.tryLock(5, 10, TimeUnit.SECONDS);
lock.lock(10, TimeUnit.SECONDS);
//不走if (leaseTime > 0)逻辑
lock.tryLock(0, -1, TimeUnit.SECONDS);
lock.lock(-1, TimeUnit.SECONDS);
//lock()不带参数,默认leaseTime = -1
lock.lock();
leaseTime不大于0时,会执行scheduleExpirationRenewal(threadId);
当oldEntry == null时,执行renewExpiration(); 就是个定时器,即是看门狗核心逻辑了。
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock {} expiration", getRawName(), e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
其中有下面红圈这段逻辑,也就是看门狗之所以在三分之一的时候执行的原因, 没有自定义时
config.setLockWatchdogTimeout(10000); // 设置锁的看门狗超时时间为10秒
默认就是30秒 除以3 也就是10秒时执行。
再看几段实现分布式锁核心的LUA脚本
1.拿锁LUA。
ChaGPT解释此段LUA:
这是 Redisson 中 RedissonLock
类实现的 tryLockInnerAsync()
方法。该方法用于尝试获取锁,它会先检查锁是否已经被其他线程持有,如果没有被持有,则将当前线程设置为锁的持有者,并返回 null;否则返回当前线程需要等待的时间,以便稍后再次尝试获取锁。
具体地说,Lua 脚本的逻辑是这样的:
- 如果锁不存在或者锁已经被当前线程持有(使用 hash 存储锁的持有者),则将当前线程作为锁的持有者,并且设置锁的过期时间,并返回 null。这里的代码具体操作包括使用
HEXISTS
命令检查锁的持有情况,使用HINCRBY
命令来设置持有者和持有计数器并提高计数器的值,以及使用PEXPIRE
命令来设置锁的过期时间。 - 如果锁已经被其他线程持有,则返回当前线程需要等待的时间,以便稍后再次尝试获取锁。这里使用
PTTL
命令来查询锁的过期时间,并返回给调用方。
需要注意的是,因为 tryLockInnerAsync
方法的实现是原子的,所以它可以保证在分布式环境下的正确性。同时,它也支持重入锁的功能,因为它保留了锁的持有者和持有计数器的值。
2.看门狗续租LUA
ChaGPT解释此段LUA:
这是 Redisson 中 RedissonLock
类实现的 renewExpirationAsync(long threadId)
方法。该方法用于续期锁的过期时间,以避免锁在使用期间失效。首先,该方法会检查当前线程是否持有锁(通过 hash 存储锁的持有者),如果当前线程不是锁的持有者,则会直接返回 false 表示续期失败。如果当前线程是锁的持有者,则会将锁的过期时间重置为 internalLockLeaseTime
毫秒,并返回 true 表示续期成功。
具体来说,Lua 脚本的逻辑是这样的:
- 如果当前线程不是锁的持有者,则直接返回 0(false)。
- 如果当前线程是锁的持有者,则将锁的过期时间重置为
internalLockLeaseTime
毫秒,并返回 1(true)。
3.解锁LUA
这是 Redisson 中 RedissonLock
类实现的 unlockInnerAsync(long threadId)
方法。该方法用于释放锁,首先它会检查当前线程是否持有锁(通过 hash 存储锁的持有者),如果当前线程不是锁的持有者,则直接返回 null;如果当前线程是锁的持有者,则将锁的计数器减一,如果减完之后计数器大于 0,则续期锁的过期时间,并返回 false 表示未成功释放锁;否则则删除该锁,并发布一个消息通知其他竞争者可以获取到该锁了,最后返回 true 表示成功释放锁。
具体来说,Lua 脚本的逻辑是这样的:
- 如果当前线程不是锁的持有者,则直接返回 null。
- 如果当前线程是锁的持有者,则使用
HINCRBY
命令将锁的计数器减一,并获取计数器的当前值。 - 如果锁的计数器大于 0,则使用
PEXPIRE
命令续期锁的过期时间,并返回 0(false)。 - 如果锁的计数器已经为 0,则使用
DEL
命令删除该锁,并使用PUBLISH
命令发布一个消息通知其他竞争者可以获取到该锁了,最后返回 1(true)。
4.预留了一个强制暴力解锁的方法给用户
//没有释放锁,导致整个系统的性能下降甚至故障,必须强行停止这个锁时 使用lock.forceUnlock();
这是 Redisson 中 RedissonLock
类实现的 forceUnlockAsync()
方法。该方法用于强制释放锁,取消锁定的状态。在方法内部,它会调用 cancelExpirationRenewal(null)
方法来停止看门狗线程的续期任务。然后使用 evalWriteAsync
方法执行 Lua 脚本,通过将锁的名字和订阅通道作为参数传入,删除锁并返回结果,同时发布一条消息到订阅通道上,表示这个锁被释放了。
具体地说,Lua 脚本的逻辑是这样的:
- 使用
DEL
命令删除锁,如果返回值为 1,则说明锁被成功删除,否则说明当前线程并没有持有这个锁,直接返回 0。 - 如果锁被成功删除,使用
PUBLISH
命令发布一条消息到订阅通道上,表示这个锁被释放了,并返回 1。
在所有操作锁的地方使用LUA脚本,单线程,天然线程安全。