前言
上一章介绍了分布式锁的基本概念和一些实现方式,其中就有Redis的实现方案。Redisson是Redis官方推荐的Java客户端,Redisson有几种不同锁的实现,包括公平锁,非公平锁,联合锁【MultiLock】,红锁【RedLock】,读写锁【ReadWriteLock】,后续几章我们将一一介绍。
介绍
日常使用的时候,我们通过Redisson获取一个分布式锁,默认是非公平锁。也就是【RedissonLock】,先看看类说明:
/**
* Distributed implementation of {@link java.util.concurrent.locks.Lock}
* Implements reentrant lock.<br>
* Lock will be removed automatically if client disconnects.
* <p>
* Implements a <b>non-fair</b> locking so doesn't guarantees an acquire order.
*
* @author Nikita Koksharov
*
*/
可以看出RedissonLock是非公平可重入锁的一个实现,当客户端失联以后,会自动释放锁。注意两个关键词,可重入和非公平,在后续的源码分析中,会对其进行解释。
获取锁的步骤一般分为几个步骤:1.尝试获取锁;2.判断是否获取成功,并执行业务代码;3.释放锁。如下所示:
RLock testLock = redissonClient.getLock("TEST_LOCK");
try{
// 尝试获取锁
boolean lock = testLock.tryLock(10, 100, TimeUnit.SECONDS);
if (lock){
// 执行业务代码
}
}finally {
if (testLock.isLocked()){
log.info("wait lock :{}",testLock.getHoldCount());
// 释放锁
testLock.unlock();
}
}
获取锁
什么是可重入?
前文提到,RedissonLock是可重入的。那么什么是可重入?是说对于同一个资源,在客户端线程A拿到锁以后,在释放锁之前,可以多次成功获取锁;不需要等待当前锁释放,也不与其他线程竞争。从源码中可以看出参数中带上了当前线程ID,就是可重入的一个重要标识。
/**
* Tries to acquire the lock by thread with specified <code>threadId</code> and <code>leaseTime</code>.
* Waits up to defined <code>waitTime</code> if necessary until the lock became available.
*
* Lock will be released automatically after defined <code>leaseTime</code> interval.
*
* @param threadId id of thread
* @param waitTime time interval to acquire lock
* @param leaseTime time interval after which lock will be released automatically
* @param unit the time unit of the {@code waitTime} and {@code leaseTime} arguments
* @return <code>true</code> if lock acquired otherwise <code>false</code>
*/
@Override
public RFuture<Boolean> tryLockAsync(long waitTime, long leaseTime, TimeUnit unit) {
// 当前线程的id
long currentThreadId = Thread.currentThread().getId();
return tryLockAsync(waitTime, leaseTime, unit, currentThreadId);
}
参数解释
- waitTime:等待获取锁的时间
- leaseTime:自动释放锁的时间,如果不设置此参数,默认是Config.lockWatchdogTimeout = 30 * 1000,30秒。
这里需要注意EVAL_NULL_BOOLEAN和EVAL_LONG两种语义,
EVAL_NULL_BOOLEAN
底层实现是RFuture tryAcquireOnceAsync(...),返回boolea值,只执行一次,等待过程中,不进行重试获取锁。
EVAL_LONG
底层实现是RFuture tryAcquireAsync(...),返回具体等待时间,在等待过程中,一直等待其他客户端释放锁的信号,一旦有此信号发出,立即执行重新获取锁动作。
下面是获取锁的具体方法,依然是用LUA脚本实现。
<T> RFuture<T> tryLockInnerAsync(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));
}
首先看看其中的几个指令,hincrby,hexists,pexpire。其中hincrby是给hash表的字段加上指定的增量值,hexists是判断hash表中是否存在指定的字段,可以看出Redisson将锁的持有状态记录在hash表中,每重入一次加一。
执行过程:
- 判断资源是否被锁定,redis.call('exists', KEYS[1]),如果返回1,则表示资源已经被其他线程持有,继续执行后续逻辑;如果没有被锁定,则更新锁的持有者,并设定过期时间;
- 判断资源是否被当前线程锁定,如果是,则累加持有次数,并重置过期时间;
- 如果被持有的不是当前线程,则返回当前资源锁的过期时间,return redis.call('pttl', KEYS[1]);
- 返回的是RFuture,当lua脚本return nil时,返回的就是0或者true。当return pttl时间时,返回的就是具体超时时间数值或者false。
注意第一行的变量【internalLockLeaseTime】,在后续锁释放时还会用到。
什么是非公平?
从获取锁的过程中,哪里体现了非公平呢?非公平锁,不能保证请求的顺序,就是说A和B先后请求同一个key的锁,A可能在B之后拿到锁,因为线程在等待拿锁的过程中,没有一个队列来维护其先后顺序,而是由各个线程进行竞争,先到先得。
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 等待限定的时间,如果超时之前收到锁释放的通知,则继续竞争,否则返回失败
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
//... to do sth.
acquireFailed(threadId);
return false;
}
// ...
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// 循环竞争锁
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
// 阻塞等待锁释放之后,再次尝试获取锁
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
// ...
}
释放锁
一般在业务代码中,获取锁和释放锁需要成对出现,否则会出现问题。
手动释放
业务逻辑执行完成以后,在finally代码块中,执行锁释放操作
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));
}
释放逻辑:
- 判断当前资源是否有锁,如果没锁【可能已经自动释放】直接返回成功;
- 计算当前线程持有锁次数减一之后的值,如果持有次数任然大于零,则重置超时时间;超时时间是internalLockLeaseTime,在获取锁时设置的参数,如果没有设置,则默认为lockWatchdogTimeout = 30 * 1000;
- 如果锁持有次数小于等于0,则删除资源锁,并发布锁释放指令,通知其他线程进行锁竞争。
强行释放
在多次重入之后,如果lock/unlock在代码中不是成对出现,那么会导致手动释放不能成功,这时就需要强制释放了,无论该线程重入了多少次。
@Override
public RFuture<Boolean> forceUnlockAsync() {
cancelExpirationRenewal(null);
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('del', KEYS[1]) == 1) then "
+ "redis.call('publish', KEYS[2], ARGV[1]); "
+ "return 1 "
+ "else "
+ "return 0 "
+ "end",
Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE);
}
源码中直接删除资源锁,如果删除成功,则通知其他线程进行锁竞争,否则直接返回。
自动释放
自动释放依赖redis的key过期机制,前面在获取锁时,设定了过期时间,一旦时间过期,且资源锁还未释放的情况下,由redis进行清除。