Redisson的分布式锁

本文介绍如何使用Redisson的分布式锁解决续期问题。通过Maven配置引入依赖后,在SpringBoot中定义配置类,并利用Redisson提供的API进行加锁和解锁操作。文章详细解析了Redisson锁的工作原理,包括加锁、解锁及续期机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近想使用redisson的分布式锁去替换系统中的redis分布式锁从而解决续期问题,查看了源码,发现其原理还是比较容易理解的。

一、Maven配置 

	<dependency>
		<groupId>org.redisson</groupId>
		<artifactId>redisson</artifactId>
		<version>3.13.4</version>
	</dependency>

 

二、Springboot定义配置类 

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // config.useClusterServers().addNodeAddress("redis://" + host + ":" + port); // 分片集群方式
        SingleServerConfig server = config.useSingleServer();
        config.setLockWatchdogTimeout(5 * 1000L);
        server.setAddress("redis://" + host + ":" + port);
        server.setPassword(password);
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

 

三、API 

RedissionClient交互于Redis和Java。其常用的实现类为Redisson,上锁/解锁操作的API也很简单:
RLock lock = redissonClient.getLock("锁的key");
lock.lock();
lock.unLock();

 

四、源码解读

redissionClient.getLock(“锁的名称”);本质上是创建了一个RLock。 

RLock lock =new RedissonLock(connectionManager.getCommandExecutor(), name);

1、加锁RLock.lock 

/**
 *
 * @param leaseTime 锁的有效期
 * @param unit 时间单位
 * @param interruptibly 中断标识位
 * @throws InterruptedException
 */
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
	long threadId = Thread.currentThread().getId();
	// 尝试获取锁的逻辑,返回值为表明redis中此锁还剩余的有效时长
	Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
	// 如果锁的有效时间为空,证明上锁成功
	if (ttl == null) {
		return;
	}

	RFuture<RedissonLockEntry> future = subscribe(threadId);
	if (interruptibly) {
		commandExecutor.syncSubscriptionInterrupted(future);
	} else {
		commandExecutor.syncSubscription(future);
	}

	/**
	 * 自旋的方式重试获取锁
	 */
	try {
		while (true) {
			ttl = tryAcquire(-1, leaseTime, unit, threadId);
			// 如果锁的有效时间为空,证明上锁成功
			if (ttl == null) {
				break;
			}

			// future.getNow().getLatch() 底层返回一个信号量Semaphore
			// 在ttl的时间内去尝试获取许可
			// 获取不到则阻塞等待信号量的释放或者ttl之后再去执行下面代码===> sleep(ttl)
			if (ttl >= 0) {
				try {

					future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
					if (interruptibly) {
						throw e;
					}
					future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
				}
			} else {
				if (interruptibly) {
					future.getNow().getLatch().acquire();
				} else {
					future.getNow().getLatch().acquireUninterruptibly();
				}
			}
		}
	} finally {
		unsubscribe(future, threadId);
	}
}

那我们看看核心方法tryAcquire是干什么的

 

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
	return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

/**
 * 
 * @param waitTime 等待时长
 * @param leaseTime 锁的时长
 * @param unit 时间单位
 * @param threadId 线程ID
 * @param <T>
 * @return
 */
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
	if (leaseTime != -1) {
		// 如果参数中设置了锁的时长则直接通过lua脚本去尝试创建redis中的节点,并设置时长
		return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
	}

	// 未设置时长的情况下,使用看门狗配置的时长;lua脚本设置redis节点
	RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
			commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
			TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
	ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
		if (e != null) {
			return;
		}

		// 返回值为空,表明设置成功,则使用看门狗机制为锁续期
		if (ttlRemaining == null) {
			scheduleExpirationRenewal(threadId);
		}
	});
	return ttlRemainingFuture;
}

分布式锁获取的lua脚本

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
	internalLockLeaseTime = unit.toMillis(leaseTime);

	return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
			"if (redis.call('exists', KEYS[1]) == 0) then " +
					"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
					"redis.call('pexpire', KEYS[1], ARGV[1]); " +
					"return nil; " +
					"end; " +
					"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
					"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
					"redis.call('pexpire', KEYS[1], ARGV[1]); " +
					"return nil; " +
					"end; " +
					"return redis.call('pttl', KEYS[1]);",
			Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

 续期操作

/**
 * 定时续期操作
 * @param threadId
 */
private void scheduleExpirationRenewal(long threadId) {
	// 新建包装续期操作的任务实体
	ExpirationEntry entry = new ExpirationEntry();
	// 放入实体map
	ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
	if (oldEntry != null) {
		// 设置线程ID
		oldEntry.addThreadId(threadId);
	} else {
		// 设置线程ID
		entry.addThreadId(threadId);
		// 续期
		renewExpiration();
	}
}

/**
 * 续期
 */
private void renewExpiration() {
	// 从任务实体map中获取本次任务实体
	ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
	if (ee == null) {
		return;
	}
	// 封装本次任务, 定时为看门狗配置时间/3
	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;
			}

			// 判断锁是否还在
			RFuture<Boolean> future = renewExpirationAsync(threadId);
			future.onComplete((res, e) -> {
				if (e != null) {
					log.error("Can't update lock " + getName() + " expiration", e);
					return;
				}

				if (res) {
					// 重新激活任务
					renewExpiration();
				}
			});
		}
	}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

	// 任务实体设置任务
	ee.setTimeout(task);
}

判断锁是否还在的lua脚本 

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
	return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
			"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
					"redis.call('pexpire', KEYS[1], ARGV[1]); " +
					"return 1; " +
					"end; " +
					"return 0;",
			Collections.singletonList(getName()),
			internalLockLeaseTime, getLockName(threadId));
}

 

2、解锁RLock.unLock 

/**
 * 同步解锁
 * @param threadId 线程ID
 * @return
 */
public RFuture<Void> unlockAsync(long threadId) {
	RPromise<Void> result = new RedissonPromise<Void>();
	// lua脚本删除redis中的节点
	RFuture<Boolean> future = unlockInnerAsync(threadId);

	future.onComplete((opStatus, e) -> {
		// 取消任务的续期
		cancelExpirationRenewal(threadId);

		if (e != null) {
			result.tryFailure(e);
			return;
		}

		if (opStatus == null) {
			IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
					+ id + " thread-id: " + threadId);
			result.tryFailure(cause);
			return;
		}

		result.trySuccess(null);
	});

	return result;
}

 lua脚本删除redis中的节点

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(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(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

取消任务续期 

void cancelExpirationRenewal(Long threadId) {
	// 获取任务实体
	ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
	if (task == null) {
		return;
	}

	if (threadId != null) {
		// 任务取消线程的绑定
		task.removeThreadId(threadId);
	}

	if (threadId == null || task.hasNoThreads()) {
		Timeout timeout = task.getTimeout();
		if (timeout != null) {
			// 取消任务
			timeout.cancel();
		}
		// 移除任务实体
		EXPIRATION_RENEWAL_MAP.remove(getEntryName());
	}
}

 

五、流程图

 加锁原理

解锁原理

 

 六、思考

Redisson在获取不到锁的情况下,默认是一直阻塞自旋的,如果业务中不想一直等待,该如何处理呢?

其实很简单,我们只要通过反射调用它尝试获取锁的方法,从而规避自旋部分即可。

/**
 * 返回获取锁的状态,true表示上锁成功
 *
 * @param lockKey
 * @return
 */
public boolean lockBackState(String lockKey) {
	try {
		RLock lock = redissonClient.getLock(lockKey);
		RedissonLock l = (RedissonLock) lock;
		Method method = l.getClass().getDeclaredMethod("tryAcquire", long.class, long.class, TimeUnit.class, long.class);
		method.setAccessible(true);
		Object invoke = method.invoke(l, -1L, -1L, null, Thread.currentThread().getId());
		return invoke == null;
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
}

 

欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!

公众号:帝都的雁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值