开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
众所周知Redisson实现了分布式锁,分布式读写锁等。相对于zookeeper,数据库乐观锁等提供了相对高的性能,充分利用redis内存数据库的优势,使用netty进行交互。封装了非常完善的一套分布式锁的操作机制。
底层
底层是利用redis中的lua脚本具有原子性的操作,实现的所有关于分布式锁相关的操作。本文会集中分析redis中lua脚本是如何解决分布式场景的各种问题。
核心方法
核心类是RedissionLock,关于lua基本的操作基本都封装在这个类中。
资源加锁
核心是一个异步加锁方法
<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));
}
redis.call('exists', KEYS[1]) == 0判断资源key是否存在,不存在加锁redis.call('hincrby', KEYS[1], ARGV[2], 1)使用hash结构存储,根据线程id自定义一个子key,设置值为1redis.call('pexpire', KEYS[1], ARGV[1])设置过期时间return nil;返回空
redis.call('hexists', KEYS[1], ARGV[2]) == 1可重入锁的判断,如果是当前线程再次获取锁,就允许获取锁redis.call('hincrby', KEYS[1], ARGV[2], 1)将当前线程的持有的值加1redis.call('pexpire', KEYS[1], ARGV[1])重新设置过期时间return nil;返回空
return redis.call('pttl', KEYS[1])
可以看到上面脚本,加锁成功后返回空。相应的可以看到返回空时代表加锁成功
其他线程进来时会返回锁还剩余的时间。
锁续期
分布式环境加锁时,可能会出现某些线程执行时间过长,超过了锁设置的有效期,从而导致不同线程又获取到了锁。导致资源竞争失败的情况。
开关
是否设置有释放时间来决定是否续期,如果没有设置,默认锁的时间是30S,并且会进行续期操作。
有效期
锁的时间:默认加锁时间private long lockWatchdogTimeout = 30 * 1000; 30S。
续期操作
使用的是看门狗的方式来进行有效期重设,保证分布式锁一直被同一个线程持有。
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);
}
}
}
}
重新设置有效期方法
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 " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration(); // 设置成功后,在递归调用自己。延迟时间是超时时间的1/3,即是10S。
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
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));
}
- 上述异步方法只是简单的将初始的过期时间,重新进行设置。
- 设置成功后,再递归调用自己。延迟时间是超时时间的1/3,即是10S。
- 上面看门狗的重复调用逻辑是利用netty中时间轮类来延迟递归调用,减少时钟的消耗。
线程阻塞
其他没有获取锁的线程会被同步阻塞,等到锁释放后,重新竞争锁。下面分析一下线程没有获取到锁后如何操作。
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
如果线程加锁时间超过了等待时间,直接放弃阻塞,在分布式公平锁中会有额外的队列处理。
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
- 采用循环来进行锁的竞争
ttl = tryAcquire(waitTime, leaseTime, unit, threadId)再次竞争,看是否加锁成功if (ttl == null) {return true;}如果加锁成功,直接返回true- 如果超时时间小于剩下的等待时间,返回加锁失败
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);- 采用key有效期过后,进行尝试获取锁,内部初始化了一个为0的信号量,用来阻塞当前线程。
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS)- 采用等待时间过后,进行尝试获取锁,内部初始化了一个为0的信号量,用来阻塞当前线程。
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
使用了redis的pubSub机制,进行监听相应key的过期和删除操作。
pubSub.subscribe(getEntryName(), getChannelName())
对资源进行订阅 LockPubSub中会进行消息的订阅
protected void onMessage(RedissonLockEntry value, Long message) {
if (message.equals(UNLOCK_MESSAGE)) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute != null) {
runnableToExecute.run();
}
value.getLatch().release();
} else if (message.equals(READ_UNLOCK_MESSAGE)) {
while (true) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute == null) {
break;
}
runnableToExecute.run();
}
value.getLatch().release(value.getLatch().getQueueLength());
}
}
- 解锁消息时,释放上面提到的信号量中的数据,再循环中会继续尝试加锁。
资源解锁
尽量减少锁的粒度,在使用完后,进行锁的释放。分为下面两个步骤
锁的删除
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
- 锁不存在时,直接返回空
- 考虑重入锁的情况,解锁时数量减1,如果减少后的数量还是大于0,返回false。
- 最后数量为0时,进行key的删除操作,同步调用redis中的publish指令,对客户端进行异步通知,返回true
锁续期删除
protected 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());
}
}
时间轮中取消该任务即可,下一步也同步删除缓存中的资源名称。
加锁,锁续期,解锁都是通过lua脚本来操作资源key的独占操作。利用lua的原子性来保证分布式锁的互斥。
读写锁
单独使用分布式锁,在读多写少的场景会影响一部分性能,可以采用分布式读写锁来进行性能上的优化。 主要是重写了加锁和解锁的lua脚本。
读加锁
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('set', KEYS[2] .. ':1', 1); " +
"redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
"local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local key = KEYS[2] .. ':' .. ind;" +
"redis.call('set', key, 1); " +
"redis.call('pexpire', key, ARGV[1]); " +
"local remainTime = redis.call('pttl', KEYS[1]); " +
"redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1])); " +
"return nil; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getRawName(), getReadWriteTimeoutNamePrefix(threadId)),
unit.toMillis(leaseTime), getLockName(threadId), getWriteLockName(threadId));
}
- SET
redis.call('hset', KEYS[1], 'mode', 'read')设置资源模式为读模式redis.call('hset', KEYS[1], ARGV[2], 1)设置资源值为1redis.call('set', KEYS[2] .. ':1', 1)拼接读写key后面加1,同步设置值为1redis.call('pexpire', KEYS[2] .. ':1', ARGV[1])拼接读写key后面加1,设置有效期redis.call('pexpire', KEYS[1], ARGV[1])设置资源的有效期
- condition
(mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1)- 如果是读锁或者写锁且写锁的key值为1,读锁碰到读锁加锁直接通过
local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1)锁的值加1local key = KEYS[2] .. ':' .. ind;拼接锁的次数keyredis.call('set', key, 1)设置锁的次数值为1redis.call('pexpire', key, ARGV[1])设置次数key的过期时间local remainTime = redis.call('pttl', KEYS[1])获得资源的过期时间redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1]))设置资源的过期时间,取剩余时间和传入过期时间的最大值
可以看出上述脚本,当读锁加锁时,如果是读锁持有,直接返回加锁成功,同步锁加锁次数(包含读锁+写锁)。
读释放锁
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local maxRemainTime = -3; " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " +
"maxRemainTime = math.max(remainTime, maxRemainTime);" +
"end; " +
"end; " +
"end; " +
"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +
"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; ",
Arrays.<Object>asList(getRawName(), getChannelName(), timeoutPrefix, keyPrefix),
LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
}
-
local mode = redis.call('hget', KEYS[1], 'mode')查询key的模式 -
"if (mode == false) then " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end; "如果key不存在,代表锁过期了,通知锁进行解锁操作。 -
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " + "if (lockExists == 0) then " + "return nil;" + "end; "如果锁的加锁次数已经为0,直接返回 -
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1)查询锁次数减1后的余值 -
"if (counter == 0) then " + "redis.call('hdel', KEYS[1], ARGV[2]); " + "end;"如果返回值为0,则删除key,因为是读锁,所以不需要进行publish通知 -
redis.call('del', KEYS[3] .. ':' .. (counter+1))进行key的删除 -
redis.call('del', KEYS[1]);删除key -
redis.call('publish', KEYS[2], ARGV[1])publish通知 中间有一大段计算剩余时间逻辑,更新key的有效期。应该是为了多次读锁时,更新资源key的有效期。
读锁续期
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
"if (counter ~= false) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " +
"end; " +
"end; " +
"end; " +
"end; " +
"return 1; " +
"end; " +
"return 0;",
Arrays.<Object>asList(getRawName(), keyPrefix),
internalLockLeaseTime, getLockName(threadId));
}
如果锁的数量不为0时,将所有的锁的超时时间设置为新的间隔时间,默认是30S。
写加锁
遵循写写互斥,写读互斥原则。修改redis中的mode为write
- 只有在读锁完全释放时,才会去加锁,且会互斥其他的写锁和读锁。
- 如果是写模式下,进行重入判断,否则进行线程阻塞。
写释放锁
- 锁不存在,直接释放锁
- 进行锁的次数减少,如果次数为0,进行key的删除和资源监听的通知
写锁续期
跟读锁续期是一样的
redission还实现了分布式场景中的公平锁,事务锁,事务读锁,事务写锁。