我们在使用分布式锁的时候,Redis是首选,java封装的redisson是一个很好的使用工具类,使用也很简单。
但是他的原理你清楚吗?在这里提几个问题: 1、锁的键值是什么? 2、锁是如果保证一直有效的? 3、锁过期,锁重入有这方面的机制吗?
在这里我们就要梳理Redisson的源码来解析,通过一层一层的深入我们看源码在 从org.redisson.RedissonLock#tryLock()入手深入 第一个比较复杂的方法调用就是 org.redisson.RedissonLock#tryAcquireOnceAsync 这个方法掉了加锁方法tryLockInnerAsync,与后续处理的方法scheduleExpirationRenewal
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Boolean> acquiredFuture;
// Redis加锁处理
// 默认leaseTime -1 不进入
if (leaseTime > 0) {
acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
// 默认进入
acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
// lock acquired
if (acquired) {
// 默认leaseTime -1 不进入
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 进行锁续期处理
scheduleExpirationRenewal(threadId);
}
}
return acquired;
});
return new CompletableFutureWrapper<>(f);
}
org.redisson.RedissonLock#tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
这里面在加锁的的时候包含了lua脚本,我们来解析这个脚本
-- 通过上面方法的入参我们对入参进行解读
-- KEYS[1] 表示键值
-- ARGV[1] 过期时间
-- ARGV[2] 表示线程ID
-- 接借来进行代码解读
--判断键值是否存在,0表示不存在证明没有有加锁,可以正常上锁
if (redis.call('exists', KEYS[1]) == 0) then
--Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值。如果哈希表的 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。这里添加键值
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 这里加入锁的过期时间,防止服务宕机后,无法释放锁
redis.call('pexpire', KEYS[1], ARGV[1]);
-- 加锁成功不需要等待
return nil;
end;
-- 判断当前键值是否存在当前线程id ,防止同一个服务的不同线程抢锁
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]);
通过上面我们了解到加锁的处理方式与唯一值的关系,保证了加锁的互斥性
接下来我们深入org.redisson.RedissonBaseLock#scheduleExpirationRenewal
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
try {
// 关键方法
renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
cancelExpirationRenewal(threadId);
}
}
}
}
继续深入 org.redisson.RedissonBaseLock#renewExpiration
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 关键点 开辟一个新的定时任务进行,超过锁的有效时长的1/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;
}
// 真正的锁续期方法
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
// 表示在有效时长的1/3后执行
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
真正的锁续期方法 org.redisson.RedissonBaseLock#renewExpirationAsync 充实锁的有效时长
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。同时解答了上面的问题。 新人编辑,自我认识,多多指教!!!