1 加锁
加锁的方法入口:Redisson的加锁方法有两个,tryLock
和 lock
1.1 tryLock
tryLock的主要流程有两步:
第一步 调用tryAcquire
,线程获取到锁返回成功
第二步 如果没有获取到锁,并且等待时间还没过,继续循环拿锁,同时监听锁是否被释放
@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();
// 1 调用tryAcquire,尝试获取锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 申请锁的耗时如果大于等于最大等待时间,则申请锁失败.
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
current = System.currentTimeMillis();
/**
* 2.订阅锁释放事件,并通过 await 方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题:
* 基于信息量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争.
*
* 当 this.await 返回 false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败.
* 当 this.await 返回 true,进入循环尝试获取锁.
*/
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// await 方法内部是用 CountDownLatch 来实现阻塞,获取 subscribe 异步执行的结果(应用了 Netty 的 Future)
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(threadId);
return false;
}
try {
// 计算获取锁的总耗时,如果大于等于最大等待时间,则获取锁失败.
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
/**
* 3.收到锁释放的信号后,在最大等待时间之内,循环一次接着一次的尝试获取锁
* 获取锁成功,则立马返回 true,
* 若在最大等待时间之内还没获取到锁,则认为获取锁失败,返回 false 结束循环
*/
while (true) {
long currentTime = System.currentTimeMillis();
// 再次尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 超过最大等待时间则返回 false 结束循环,获取锁失败
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
/**
* 6.阻塞等待锁(通过信号量(共享锁)阻塞,等待解锁消息):
*/
currentTime = System.currentTimeMillis();
// 在 min(ttl,等待时间time)内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可)
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// 更新剩余的等待时间 (最大等待时间-已经消耗的阻塞时间)
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
// 7.无论是否获得锁,都要取消订阅解锁消息
unsubscribe(subscribeFuture, threadId);
}
return get(tryLockAsync(waitTime, leaseTime, unit));
}
1.2 tryAcquire
tryAcquire
主要调用了tryAcquireAsync
。对于tryLock
方法来说,leaseTime != -1
, 直接调用tryLockInnerAsync
方法获取锁。
// leaseTime 锁的持有时间
// threadId 当前线程的ID
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
// tryLock设置了锁的等待时长,就直接调用tryLockInnerAsync方法获取锁
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// lock 的 leaseTime 传入的是 -1 ,所以走下面的这个分支
// 1)尝试加锁 tryLockInnerAsync
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS,
threadId,
RedisCommands.EVAL_LONG);
//2)注册监听事件
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// 3)获取锁成功的话,给锁延长过期时间
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
1.3 tryLockInnerAsync
加锁脚本tryLockInnerAsync
中,使用Hash结构存储数据
-- 不存在key时
if (redis.call('exists', KEYS[1]) == 0) then
-- 创建锁,锁唯一标识 对应的 value设置为1,设置过期时间,返回nil
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 存在key 并且锁唯一标识匹配
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 线程重入次数++,重置过期时间,返回nil
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 存在key 锁唯一标识不匹配的场景,返回锁的过期时间
return redis.call('pttl', KEYS[1]);
参数含义
参数 | 含义 |
---|---|
KEYS[1] | 锁名 |
ARGV[1] | 持有锁的有效时间:毫秒 |
ARGV[2] | 锁唯一标识:客户端ID(UUID)+线程ID |
流程图
1.3 lock
lock
主要实现加锁和锁续约。没有手动设置锁过期时长
的参数,lock
方法调用lockInterruptibly
方法来获取锁。lockInterruptibly
主要流程有两步
第一步:预设30秒的过期时长,调用tryAcquire
去获取锁 ( tryAcquire
代码请见1.2 tryAcquire)
对于`lock`方法,`leaseTime == -1`,所以`tryAcquire`里面主要有两个逻辑:
- 预设30秒的过期时长,然后调用
tryLockInnerAsync
获取锁, - 如果发现拿到锁了, 开启一个监听器,调用
scheduleExpirationRenewal
不断去刷新该锁的过期时长
第二步: 获取锁失败,循环继续获取锁
public void lock() {
try {
// 调用lockInterruptibly
this.lockInterruptibly();
} catch (InterruptedException var2) {
Thread.currentThread().interrupt();
}
}
public void lockInterruptibly() throws InterruptedException {
// 调用lockInterruptibly, leaseTime = -1
this.lockInterruptibly(-1L, (TimeUnit)null);
}
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
// 获取当前线程ID
long threadId = Thread.currentThread().getId();
// 尝试获取锁的剩余时间
// ttl为空,说明没有线程持有该锁,线程加锁成功,直接返回
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
// ttl不为空,说明加锁失败
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
// 再此尝试获取锁的剩余时间,如果为null, 跳出循环
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
// waiting for message 如果ttl >= 0 说明有其他线程持有该锁
if (ttl >= 0L) {
// 获取信号量,尝试加锁,设置最大等待市场为ttl
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// 如果ttl小于0(-1 ,-2 ) 说明已经过期,直接获取
this.getEntry(threadId).getLatch().acquire();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
1.4 锁续约scheduleExpirationRenewal
当一个线程持有了一把锁,并且未设置超时时间,Redisson默认配置了leaseTime =30S
,开启开启一个定时任务,每internalLockLeaseTime / 3
对该锁进行一次续约,直到任务完成再删除锁。
这就是WatchDog实现的基本思路。
private void scheduleExpirationRenewal(final long threadId) {
// expirationRenewalMap是一个ConcurrentMap,存储标志为"当前线程ID:key名称"的任务
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 检测锁是否存在的lua脚本,存在的话就用pexpire命令刷新过期时长
RFuture<Boolean> future = commandExecutor.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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
protected String getEntryName() {
return this.id + ":" + this.getName();
}
1.5 加锁流程
3 解锁
3.1 unlock
unlock
主要调用unlockInnerAsync
来释放锁。
public void unlock() {
// 发起释放锁的命令请求
Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
} else {
if (opStatus) {
// 成功释放锁,取消"看门狗"的续时线程
this.cancelExpirationRenewal();
}
}
}
3.2 unlockInnerAsync
unlockInnerAsync
脚本
-- 锁不存在,广播解锁消息,返回1
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
-- 锁存在,但是唯一标识不匹配,返回nil
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 重次数大于0,重置过期时间,返回 0
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
-- 重次数为 <= 0,删除key,并广播解锁消息,返回1
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
参数
参数 | 含义 |
---|---|
KEYS[1] | 锁名 |
KEYS[2] | 解锁消息PubSub频道:redisson_lock_channel:{myLock} |
ARGV[1] | 固定值0,表示解锁消息 |
ARGV[2] | 锁的过期时间;默认值30秒 |
ARGV[3] | 锁唯一标识;同加锁流程 |
流程图