RLock锁源码解析之lock上锁操作
入口:编写一个简单的案例
/**
* 可重入锁
* 1.锁会自动续期,如果业务时间超长,运行期间Redisson会自动给锁重新添加30s,不用担心业务时间的问题,锁自动过期而导致业务发生问题
* 2、加锁的业务只要完成执行,那么就不会给当前的锁续期,即使我们不去主动的释放锁,锁在默认30s之后也会自动的删除
*
* @return
*/
@RequestMapping("/hello")
@ResponseBody
public String hello() {
//获取锁
RLock lock = redissonClient.getLock("lock");
//进行加锁
lock.lock();
try {
System.out.println("做业务逻辑的操作" + Thread.currentThread().getName());
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("释放锁成功。。" + Thread.currentThread().getName());
//释放锁
lock.unlock();
}
return "hello";
}
lock.lock();获取锁,加锁。
来到RedissonLock类中:
@Override
public void lock() {
try {
//调用有参构造方法
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
调用lock(-1, null, false); 构造方法
当前类位置:RedissonLock
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//获取当前线程的id
long threadId = Thread.currentThread().getId();
//获取锁,继续往下看源码
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
继续调用加锁的过程: Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
当前类位置:RedissonLock
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
//继续执行tryAcquireAsync 方法
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
当前类位置:RedissonLock
继续执行:tryAcquireAsync(waitTime, leaseTime, unit, threadId)
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
//传入的参数不等于-1的情况
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 等于-1的分支
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
由于我们这没有进行设置key的过期时间,默认传入的值是-1
所以我们走的是等于-1的分支
执行的代码是:
分析:传入的参数
waitTime:这里传入的默认值是-1
internalLockLeaseTime:成员属性,系统给我们默认设置为30S,Redisson会自动给锁重新添加30s,不用担心业务时间的问题,锁自动过期而导致业务发生问题,点进源码可以查看到
最终获取到的参数是:毫秒为单位,30秒的过期时间
TimeUnit.MILLISECONDS:时间单位为毫秒
threadId:当前线程id
RedisCommands.EVAL_LONG:点进源码可以查看
这里就是具体实现分布式锁(可重入锁的具体实现了)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));
}
下面我们就具体来分析下是什么含义:
分析各个含义:
:::success
1、tryLockInnerAsync()方法参数
:::
-
tryLockInnerAsync: 这是一个异步方法,试图获取锁。
-
: 这是一个泛型方法,可以返回任意类型的结果。
-
long waitTime: 等待时间,以指定的时间单位表示。
-
long leaseTime: 锁的租约时间,也就是key的过期时间,也是以指定的时间单位表示。
-
TimeUnit unit: 时间单位,例如毫秒、秒等。
-
long threadId: 线程ID,用于标识请求锁的线程。
-
RedisStrictCommand command: Redis 命令,执行锁操作。
:::success
2、evalWriteAsync()方法参数:
:::
对应的参数列表: -
getRawName():可能是锁的唯一标识符,例如锁的资源名称。
-
LongCodec.INSTANCE:可能是用于编码和解码Lua脚本参数的编解码器实例。
-
command:传入的Redis命令。
-
Lua脚本:这是Lua脚本的主体,用于执行锁逻辑。
-
Collections.singletonList(getRawName()):传给 evalWriteAsync 的键列表,这里只包含一个键。也就是我们从一开始设置的key的名称lock,通过集合工具类 Collections创建一个单数据集合。
-
unit.toMillis(leaseTime):将key的过期时间转换为毫秒,这里系统默认是30秒。这个用于后续传入脚本的中的值,是params第一个参数,这里的params是动态数组,可以传入多个。
-
getLockName(threadId):据线程ID生成的锁名称。这个用于后续传入脚本的中的值,是params第二个参数,这里的params是动态数组,可以传入多个。
:::success
3、evalWriteAsync()方法实现过程:具体的逻辑实现
::: -
通过调用 evalWriteAsync 方法执行 Redis 脚本,该脚本尝试获取锁。
-
脚本首先检查锁是否存在,如果不存在,则增加锁的计数器并设置过期时间,表示当前线程获得了锁。
-
如果锁存在且由当前线程持有,则增加计数器并更新过期时间。
-
如果锁存在但由其他线程持有,则返回锁的剩余过期时间。
:::success
4、脚本解析:lua脚本
:::
-
redis.call(‘exists’, KEYS[1]) == 0: 检查键是否存在,如果不存在则说明锁可用。
-
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1): 增加哈希表中指定字段的值,即增加锁的计数器。用的redis中的数据类型是hash。
-
redis.call(‘pexpire’, KEYS[1], ARGV[1]): 设置键的过期时间,以毫秒为单位。
-
return nil;执行完第一个if分支语句,执行成功返回nil,返回空值。
-
redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1: 检查哈希表中是否存在指定字段,即检查当前线程是否已持有锁。满足条件,往下执行。
-
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1):锁计数+1的操作。 增加哈希表中指定字段的值,即增加锁的计数器。
-
redis.call(‘pexpire’, KEYS[1], ARGV[1]): 继续设置锁的过期时间,默认系统设置参数为30秒
-
redis.call(‘pttl’, KEYS[1]): 返回键的剩余过期时间,以毫秒为单位。
KEYS[1] ARGV[1] ARGV[2] 分别传入的值是:Collections.singletonList(getRawName())、unit.toMillis(leaseTime)
- KEYS[1]: 这是表示第一个键的占位符。这里传入的值为lock ,也就是在Collections.singletonList(getRawName())获取到的值。这里传入的是"lock"字符串。 Redis 的 Lua 脚本中,KEYS 表示所有的键,索引从1开始。在这个方法中,KEYS[1] 应该表示锁的名称,即被用来标识这个锁的唯一键。这个键通常是一个 Redis 的键(key),可以是一个字符串。
- ARGV[1]: 这是表示第一个参数的占位符。这里传入的是unit.toMillis(leaseTime) 返回的值,默认是30s的过期时间。在 Redis 的 Lua 脚本中,ARGV 表示所有的参数,索引也是从1开始的。在这个方法中,ARGV[1] 表示锁的过期时间,以毫秒为单位。这个参数是通过 leaseTime 经过时间单位转换后传入的值。
- ARGV[2]: 这是表示第二个参数的占位符。ARGV[2] 应该表示当前线程的标识符,即用来标识请求锁的线程的唯一值。这个参数是通过 threadId 传入的值。
总体来说:这段代码是一个异步的尝试获取锁的方法tryLockInnerAsync()。调用执行lua脚本的方法evalWriteAsync();
总体实现可重入锁的思路为:
- 第一种情况:首先检查锁是否(key)存在,如果不存在,则增加锁的计数器并设置过期时间为30S,表示当前线程获得了锁。设置锁成功,并且返回null。
- 第二种情况:再次进来:如果锁存在且由当前线程持有,则增加计数器并更新过期时间。将锁的过期时间重置为30S,实现锁的可重入。设置成功,返回null。
- 第三种情况:如果锁存在但由其他线程持有,则返回锁的剩余过期时间。