从Redis实现分布式锁的问题延伸到Redisson的使用入门

部分文章来源:二哥Java,JavaGuide

本文从Redis的setNx实现分布式锁的问题,以及Redi本身简单实现锁的锁误删,锁提前释放,不可重入锁,死锁问题然后延伸到开源的框架Redisson的入门使用


前提知识-什么是可重入锁?

也就是我们的同一个线程可以多次获取一把锁

为什么要有可重入锁?

多次锁定

如果一个线程已经获得了可重入锁,它可以在没有释放锁的情况下,再次获取该锁。每次获取锁时,锁的内部计数器会递增

当线程调用unlock()释放锁时,计数器递减。只有当计数器减为零时,锁才会真正被释放,其他线程才有机会获得该锁

递归调用

在递归调用中,方法可能会多次尝试获取同一把锁。可重入锁允许这种行为,使得递归方法调用不会因为再次获取已经持有的锁而导致死锁。

因为有些情况,例如递归,他会重复获取同一个锁

但是递归的时候,它自己等待自己释放,那不就造成了死锁的现象?

所以我们就要有可重入锁

嵌套锁定

如果一个方法调用另一个需要同一把锁的方法,可重入锁允许这种嵌套调用,而不会导致线程阻塞


Redis实现分布式锁会有什么问题?

问题所在

默认情况下,如果使用 setnx lock true 实现分布式锁会存在以下问题:

  1. 死锁问题:setnx 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。
  2. 锁误删问题:setnx 设置了超时时间,但因为执行时间太长,所以程序没运行完,锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。
  3. 不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
  4. 无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。

而这些问题的解决方案也是不同的。

① 解决死锁问题

死锁问题可以通过设置超时时间来解决,如果超过了超时时间,分布锁会自动释放,这样就不会存在死锁问题了。也就是 setnx 和 expire 配合使用,在 Redis 2.6.12 版本之后,新增了一个强大的功能,我们可以使用一个原子操作也就是一条命令来执行 setnx 和 expire 操作了,实现示例如下:

127.0.0.1:6379> set lock true ex 30 nx
OK #创建锁成功
127.0.0.1:6379> set lock true ex 30 nx
(nil) #在锁被占用的时候,企图获取锁失败

其中 ex 为设置超时时间, nx 为元素非空判断,用来判断是否能正常使用锁的

② 解决锁误删问题

锁误删可以通过将锁标识存储到 Redis 中来解决,删除之前先判断锁归属(也就是将线程 id 存储到分布式的 value 值内,删除之前先判断锁 value 值是否等于当前线程 id),如果属于你的锁再删除,否则不删除就可以,这就解决了锁误删的问题。

但这样解决因为判断和删除是非原子操作 ,所以依旧有问题,这个问题可以通过编写lua 脚本 或使用 Redisson 框架来解决,因为他们两都能保证判断和删除的原子性。

③ 通用解决方案

以上问题有一个通用的解决方案,那就是使用 Redisson 框架来实现 Redis 分布式锁,这样既可以解决死锁问题,也可以解决锁误删、不可重入和无法自动续期的问题了。

简单来说就是锁误删,锁续期,不可重入,死锁这4个问题


如何解决分布式锁可靠性问题(红锁,联锁)

使用Redisson

Redisson实现了多种分布式锁

Redisson分布式锁种还有一个Watch Dog(看门狗)机制,当操作共享资源还没完成的时候,能够实现自动续期

一般分布式锁我们是使用Redisson 来做的

Redisson如何解决集群情况下分布式锁的可靠性

红锁

如果我们的Redis主节点,拿到锁之后宕机了
然后没有同步到其他节点,然后就选举出了一个新的Redis主节点
因为锁没有同步,所以这个新的Redis主节点还可以获取锁

我们会出现这种问题
这个时候就是 Redlock 算法, 红锁 ,来解决这个问题了

如果我们能够和 半数以上 的实例成功加锁,那么就认为客户端成功获取到了锁

其实红锁,是联锁 的一种实现

联锁

redisson.getMultiLock(锁1,锁2,锁3)


如何使用Redisson实现分布式锁

什么是Redisson

Redisson 是一个开源的用于操作 Redis 的 Java 框架。与 Jedis 和 Lettuce 等轻量级的 Redis 框架不同,它提供了更高级且功能丰富的 Redis 客户端。它提供了许多简化 Redis 操作的高级 API,并支持分布式对象、分布式锁、分布式集合等特性。

Redisson 官网地址:Redisson: Valkey and Redis Java client. Complete Real-Time Data Platform

源码地址:GitHub - redisson/redisson: Redisson - Valkey and Redis Java client. Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..

特性

  1. Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。
  2. Redisson 在为每个锁关联一个线程 ID**(保证当前线程只能释放当前线程的锁)** 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
  3. Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。

Redisson 实现分布锁

① 添加 Redisson 框架支持

如果是 Spring Boot 项目,直接添加 Redisson 为 Spring Boot 写的如下依赖:

<!-- Redisson -->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.25.2</version><!-- 请根据实际情况使用最新版本 -->
</dependency>

其他项目,访问 https://mvnrepository.com/search?q=Redisson 获取具体依赖配置。
② 配置 RedissonClient 对象

将 RedissonClient 重写,存放到 IoC 容器,并且配置连接的 Redis 服务器信息。

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclassRedissonConfig {
@Bean
public RedissonClient redissonClient() {
Configconfig=newConfig();
// 也可以将 redis 配置信息保存到配置文件
 config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
 }
}

实现公平锁

Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑),想要把它变成公平锁可使用以下代码实现:

RLocklock= redissonClient.getFairLock(lockKey); // 获取公平锁

实现读写锁

Redisson 还可以创建读写锁,如下代码所示:

RReadWriteLocklock= redissonClient.getReadWriteLock(lockKey); // 获取读写锁
lock.readLock(); // 读锁
lock.writeLock(); // 写锁

读写锁的特点就是并发性能高(读读共享、读写/写写不共享),它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。

实现联锁

Redisson 也支持联锁,也叫分布式多锁 MultiLock,它允许客户端一次性获取多个独立资源(RLock)上的锁,这些资源可能是不同的键或同一键的不同锁。当所有指定的锁都被成功获取后,才会认为整个操作成功锁定。这样能够确保在分布式环境下进行跨资源的并发控制。

联锁的实现示例如下:

// 获取需要加锁的资源
RLocklock1= redisson.getLock("lock1");
RLocklock2= redisson.getLock("lock2");
// 联锁
RedissonMultiLockmultiLock=newRedissonMultiLock(lock1, lock2);
try {
// 一次性尝试获取所有锁
if (multiLock.tryLock()) {
// 获取锁成功...
 }
} finally {
// 释放所有锁
 multiLock.unlock();
}

说一下Redisson的看门狗机制

什么是看门狗机制

Redisson 看门狗(Watchdog)机制是一种用于延长分布式锁的有效期的机制。它通过定时续租锁的方式,防止持有锁的线程在执行操作时超过了锁的有效期而导致锁被自动释放。

看门狗(Watchdog)的执行过程大致如下:

  1. 获取锁并设置超时时间:当客户端通过 Redisson 尝试获取一个分布式锁时,会使用 Redis 命令将锁存入 Redis,并设置一个初始的有效时间(即超时时间)。
  2. 启动看门狗线程:如果开启了看门狗的功能(默认开启),在成功获取锁后,Redisson 会在客户端内部启动一个后台守护线程,也就是所谓的"看门狗"定时任务定时去执行并续期。
  3. 定时检查与续期:看门狗按照预设的时间间隔(默认为锁有效时间的三分之一)周期性地检查锁是否仍然被当前客户端持有。如果客户端仍然持有锁,看门狗会调用 Redis 的相关命令或者 Lua 脚本来延长锁的有效期,确保在业务处理期间锁不会因超时而失效。
  4. 循环监控和更新:这个过程会一直持续到客户端显式地释放锁,或者由于其他原因(例如客户端崩溃、网络中断等)导致无法继续执行看门狗任务为止。
  5. 终止看门狗任务:客户端在完成业务逻辑后,会主动调用解锁方法释放锁,此时 Redisson 不仅会解除对 Redis 中对应键的锁定状态,还会同步停止看门狗的任务。

通过看门狗机制,即使在长时间运行的业务场景下,也能有效地避免由于锁超时而导致的数据不一致或其他并发控制问题,提高了系统的稳定性和可靠性。


知识扩展

看门狗实现原理

Redisson 看门狗的底层实现就是一个定时任务 ,它的看门狗默认的超时时间是 30s,不过超时时间可以使用 Config.lockWatchdogTimeout 来进行设置。

看门狗每隔 lockWatchdogTimeout/3L 会执行一次检查和续期分布式锁,它的核心实现源码如下:

privatevoidrenewExpiration() {
	// 拿到需要延期的锁信息
	RedissonBaseLock.ExpirationEntryee= (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
	// 如果锁信息不为空
	if (ee != null) {
		// 构建一个定时任务,周期为 this.internalLockLeaseTime / 3L
		// 而this.internalLockLeaseTime就是上面提到的默认超时时间30000L,也就是30s
		// 30000L / 3L,得出周期时间为 10秒,也就是说,10秒检查一次,每次都会将锁的过期时间延长至30秒
		Timeouttask=this.commandExecutor.getConnectionManager().newTimeout(newTimerTask() {
			publicvoidrun(Timeout timeout)throws Exception {
				// 获取要延期的锁信息
				RedissonBaseLock.ExpirationEntryent= (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
				// 锁存在才继续,否则退出
				if (ent != null) {
					LongthreadId= ent.getFirstThreadId();
					// 线程id不为空才继续,否则退出
					if (threadId != null) {
						// 通过lua脚本进行延期
						RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
						future.onComplete((res, e) -> {
							// 如果e不为空,代表有异常,从待延期锁信息集合中删除当前锁,并退出
							if (e != null) {
								RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
								RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
							} else {
								// 延期成功,递归调度,进入下一次延期
								if (res) {
									RedissonBaseLock.this.renewExpiration();
								}
							}
						});
					}
				}
			}
		}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
		ee.setTimeout(task);
	}
}

看门狗停止续期

Redisson 看门狗当遇到以下情况会停止自动续期:

  1. 锁被主动释放(调用 unlock 方法):当持有锁的线程主动释放锁时,Redisson 的看门狗会停止续期。这是因为锁已经不再被持有,没有必要进行续租操作。
  2. 线程意外终止:在持有锁的线程意外终止时,Redisson 的看门狗会停止续期。这是为了避免已经不再活动的线程持有锁,并防止续租请求的无效执行。
  3. 锁被其他线程抢占:当其他线程成功获取到同一把锁时,Redisson 的看门狗会停止续期。这是因为锁的持有线程发生了变化,原先持有锁的线程失去了锁的所有权,不再需要进行续租操作。
  4. Redisson 客户端连接断开:如果 Redisson 与 Redis 服务器端之间的连接断开,看门狗会停止续期。这是为了保证续租请求的可靠性,如果无法与 Redis 建立连接,就无法执行续期操作。
相关推荐
明达技术几秒前
分布式 IO 模块:水力发电设备高效控制的关键
分布式
星星点点洲16 分钟前
【操作幂等和数据一致性】保障业务在MySQL和COS对象存储的一致
java·mysql
xiaolingting32 分钟前
JVM层面的JAVA类和实例(Klass-OOP)
java·jvm·oop·klass·instanceklass·class对象
风口上的猪20151 小时前
thingboard告警信息格式美化
java·服务器·前端
水手胡巴1 小时前
oracle apex post接口
数据库·oracle
专注API从业者1 小时前
分布式电商系统中的API网关架构设计
大数据·数据仓库·分布式·架构
追光少年33221 小时前
迭代器模式
java·迭代器模式
点点滴滴的记录2 小时前
系统设计之分布式
分布式
超爱吃士力架2 小时前
MySQL 中的回表是什么?
java·后端·面试
扣丁梦想家3 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式