最近想使用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;
}
}
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁