使用分布式锁首先要创建一个Config。Config会设置传输模式transportMode为NIO,
lockWatchdogTimeout为30秒。可以设置为单一节点模式,主从订阅模式,集群模式,哨兵模式。
根据此config,可以创建RedissionClient
// 1.构造redisson实现分布式锁必要的Config Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0); // 2.构造RedissonClient RedissonClient redissonClient = Redisson.create(config); // 3.获取锁对象实例(无法保证是按线程的顺序获取到) RLock rLock = redissonClient.getLock(lockKey);
getLock()方法会创建一个RedissionLock实例,该实例有三个属性
internalLockLeaseTime会被赋值为lockWatchdogTimeout的值
CommandAsyncExecutor 一个异步执行类。
LockPubSub 锁的订阅发布,其中LockPubSub中有一个方法:
protected void onMessage(RedissonLockEntry value, Long message) {
Runnable runnableToExecute;
//如果是释放锁,value.getListeners()是一个ConcurrentLinkedQueue<Runnable>,会弹出第一个元素,然后唤醒他。
if (message.equals(UNLOCK_MESSAGE)) {
runnableToExecute = (Runnable)value.getListeners().poll();
if (runnableToExecute != null) {
runnableToExecute.run();
}
value.getLatch().release();
} else if (message.equals(READ_UNLOCK_MESSAGE)) {
while(true) {
runnableToExecute = (Runnable)value.getListeners().poll();
if (runnableToExecute == null) {
value.getLatch().release(value.getLatch().getQueueLength());
break;
}
runnableToExecute.run();
}
}
}
这里已经创建完了一个锁实例,后面要进行获取锁的操作:
try { /** * 4.尝试获取锁 * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) */ boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //成功获得锁,在这里处理业务 } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); }finally{ //无论如何, 最后都要解锁 rLock.unlock(); }
多说一嘴,tryLock的实现类有三个,RedissionLock,RedissionMultiLock(将一组锁当成一个锁来操作),RedissionSpinLock(可重入自旋锁),
这里介绍下RedissionLock的tryLock方法,底层tryLockInnerAsync方法会调用lua脚本:这里都是异步的
"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]);" 进行调用
- 在成功获取到锁的情况下,为了避免业务中对共享资源的操作还未完成,锁就被释放掉了,需要定期(锁失效时间的三分之一)刷新锁失效的时间,这里 Redisson 使用了 Netty 的 TimerTask、Timeout 工具来实现该任务调度。
- 获取锁真正执行的命令,Redisson 使用
EVAL
命令执行上面的 Lua 脚本来完成获取锁的操作: - 如果通过
exists
命令发现当前 key 不存在,即锁没被占用,则执行hset
写入 Hash 类型数据 key:全局锁名称(例如共享资源ID), field:锁实例名称(Redisson客户端ID:线程ID), value:1,并执行pexpire
对该 key 设置失效时间,返回空值nil
,至此获取锁成功。 - 如果通过
hexists
命令发现 Redis 中已经存在当前 key 和 field 的 Hash 数据,说明当前线程之前已经获取到锁,因为这里的锁是可重入的,则执行hincrby
对当前 key field 的值加一,并重新设置失效时间,返回空值,至此重入获取锁成功。 - 最后是锁已被占用的情况,即当前 key 已经存在,但是 Hash 中的 Field 与当前值不同,则执行
pttl
获取锁的剩余存活时间并返回,至此获取锁失败。
tryLockInnerAsync()方法返回的是null时,说明获取到了锁。如果锁的释放时间若未被定义,进行线程的锁续时,利用了netty的定时任务,执行时间为internalLockLeaseTime / 3。 这里说明,如果已经设置了过期时间,便不会触发看门狗机制。
if (ttlRemaining == null) { if (leaseTime != -1L) { this.internalLockLeaseTime = unit.toMillis(leaseTime); } else { this.scheduleExpirationRenewal(threadId); } }
至此,获取锁到一段落,若获取到锁,返回成功,若获取不到,首先看获取锁的等待时间是否已经耗尽,如果是,则会
this.acquireFailed(waitTime, unit, threadId); return false;
如果不是,当前线程会订阅该key,否则在等待时间耗尽前会一直获取锁,如果锁被其他线程获得,获取过时时间,到时间进行争夺。
do { long currentTime = System.currentTimeMillis(); ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId); if (ttl == null) { var16 = true; return var16; } time -= System.currentTimeMillis() - currentTime; if (time <= 0L) { this.acquireFailed(waitTime, unit, threadId); var16 = false; return var16; } currentTime = System.currentTimeMillis(); if (ttl >= 0L && ttl < time) { ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } else { ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS); } time -= System.currentTimeMillis() - currentTime; } while(time > 0L);
这个循环可以自己看明晰,解释一下这个if (ttl >= 0L && ttl < time),如果过期时间大于等待时间,会继续换取,传值为ttl,如果过期时间已经小于等待时间,依然会在过期时间继续抢夺锁,传入值为time。这里会把抢夺的线程加入到抢夺线程的node节点后。在前面的首先抢,这里的node应该就是前面那个ConcurrentLinkedQueue,这里就是cas获取节点状态,获取锁
ok,讲到解锁了,解锁关键的就是一段lua语句:
-
-- 若锁不存在:则直接广播解锁消息,并返回1
-
if (redis.call('exists', KEYS[1]) == 0) then
-
redis.call('publish', KEYS[2], ARGV[1]);
-
return 1;
-
end;
-
-- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁
-
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
-
return nil;
-
end;
-
-- 若锁存在,且唯一标识匹配:则先将锁重入计数减1
-
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-
if (counter > 0) then
-
-- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期
-
redis.call('pexpire', KEYS[1], ARGV[2]);
-
return 0;
-
else
-
-- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程
-
redis.call('del', KEYS[1]);
-
redis.call('publish', KEYS[2], ARGV[1]);
-
return 1;
-
end;
-
return nil;
在释放锁后,会发布消息,通知其他在等待的线程去争夺锁。
在获取不到锁时,线程会订阅这个锁,然后小于等待时间,会作为一个节点加入到AbstractQueuedSynchronizer同步队列中,在这里,会首先看自己的前一个节点是不是头结点,如果是头结点,说明没有等待节点,可以去尝试获取锁,获取到,返回true,否则会封装节点park等待,待前一个节点唤醒后一个节点。