1.【Redisson源码】可重入锁加锁流程

【本篇文章基于redisson-3.17.6版本源码进行分析】

Redisson的GitHub地址:

https://github.com/redisson/redisson/

我们使用git clone将源码克隆下来,编译一下,就可以阅读源码了。

编写一个单元测试,测试可重入锁的加锁逻辑:

@Test
public void testRenenLock() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    RedissonClient redissonClient = Redisson.create(config);

    RLock rLock = redissonClient.getLock("lock");

    rLock.lock();
    // ......
    rLock.unlock();
}

这里我们使用没有指定超时时间的lock()加锁方法,同时RLock还提供了可以指定超时时间的加锁方法:

// 锁将在指定的leaseTime时间间隔后自动释放
void lock(long leaseTime, TimeUnit unit);

下面我们进入RedissonLock#lock()方法:

@Override
public void lock() {
    try {
        // 默认加锁,什么参数也没有传递。但是这里会设置 leaseTime = -1。leaseTime的含义是加锁的时间
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

我们看到,如果没有指定超时时间,默认将会把leaseTime的值设置为-1:

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    // 当前线程ID
    long threadId = Thread.currentThread().getId();
    // tryAcquire()尝试获取锁
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);

    // lock acquired
    // ttl为空表示获取锁成功,则直接返回
    if (ttl == null) {
        return;
    }

    // 未能获取到锁的其它线程
    // 争抢同一把锁的其它线程,订阅channel消息
    // 发布-订阅消息:当释放锁的时候,会通过publish发布一条消息,通知其它等待这个锁的线程,我已经释放锁了,你们可以过来获取了。
    CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
    pubSub.timeout(future);
    RedissonLockEntry entry;
    if (interruptibly) {
        entry = commandExecutor.getInterrupted(future);
    } else {
        entry = commandExecutor.get(future);
    }

    try {
        // 加锁失败的话,循环重试
        while (true) {
            ttl = tryAcquire(-1, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            if (ttl >= 0) {
                try {
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                if (interruptibly) {
                    entry.getLatch().acquire();
                } else {
                    entry.getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
        unsubscribe(entry, threadId);
    }
//        get(lockAsync(leaseTime, unit));
}

tryAcquire()方法就是可重入锁加锁的关键方法:

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // tryAcquireAsync(): 异步加锁
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

tryAcquire()方法内部又是通过调用tryAcquireAsync()来异步加锁的:

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {
        // 指定了超时时间
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 未指定超时时间   internalLockLeaseTime默认就是看门狗的超时时间:30秒
        // waitTime: -1
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired

        // ttlRemaining为null, 其实就是加锁的LUA脚本中返回的nil,表示获取锁成功
        if (ttlRemaining == null) {
            if (leaseTime > 0) {   // 如果设置了超时时间,则更新internalLockLeaseTime为指定的超时时间,并且不会启动看门狗
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                // 自动续期实现,看门狗机制入口

                /**
                 * 1. 只有用户未指定锁的超时时间,看门狗才生效;如果我们指定了锁超时时间,则看门狗不会启动;
                 * 2. 获取锁成功的线程才启动看门狗
                 *
                 * lock.lock(); 开启看门狗
                 * lock.lock(5000, TimeUnit.SECONDS); 不开启看门狗
                 */
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

在本例中,我们没有指定超时时间,所以走的else分支的tryLockInnerAsync()方法,几个参数说明一下:

  • waitTime:-1;
  • internalLockLeaseTime:如果没有指定超时时间的话,使用默认时间 30000 毫秒,在RedissonLock构造方法中指定的,获取的是看门狗超时时间;
  • TimeUnit.MILLISECONDS:单位毫秒;
  • threadId:当前线程id;
  • RedisCommands.EVAL_LONG:eval

tryLockInnerAsync()加锁逻辑:

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    /**
     *  执行LUA脚本进行加锁,可以保证多个redis指令的原子性,包括可重入的处理,底层使用redis的hash数据结构
     *  例如执行getLock("lock"),redis中哈希表的数据如下:
     *  a、redis key: lock
     *  b、hash key: 7006e075-6fe4-420f-8f69-801be27d45d2:1
     *  c、hash value: 1
     *
     *  LUA脚本分为三部分:
     *  1、加锁
     *      判断锁是否存在(使用exists指令判断redis key是否存在),如果不存在,说明当前没人获取这把锁,可以直接获取(hincrby 命令创建一个新的哈希表,并指定hash的key为当前线程),并设置锁的超时时间(使用pexpire指令)。返回nil, 表示获取锁成功
     *  2、重入
     *      判断当前锁是不是自己的锁,如果是的话,则将锁的重入次数+1(hincrby命令更新哈希表的field value),并重置锁的超时时间(使用pexpire指令)。返回nil, 表示获取锁成功
     *  3、返回
     *      返回锁剩余的过期时间,单位:毫秒
     *
     *  参数说明:
     *  KEYS[1]: Collections.singletonList(getRawName()) ==> 我们指定的分布式锁的key
     *  ARGV[1]: unit.toMillis(leaseTime) ==> 锁的超时时间
     *  ARGV[2]: getLockName(threadId) ==> 每个进程的唯一ID + ":" + threadId
     *
     *  getRawName(): 我们指定的分布式锁的key
     *  getLockName(threadId): 【每个进程的唯一ID + ":" + threadId】,主要用于可重入分布式锁的判断
     */
    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));
}

从上面的代码可以看出,Redisson内部实际上就是通过一段LUA脚本来进行加锁的。下面仔细分析一下LUA脚本:

// 通过exists指令判断加锁的key是否存在,如果不存在,说明还没人加锁,可以直接进行加锁
"if (redis.call('exists', KEYS[1]) == 0) then " +   
		// 通过hincrby指令往redis中插入一个哈希结构的数据,redis key=加锁key   hash key=uuid+当前线程ID  hash value=锁的重入次数
        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
        // 通过pexpire设置锁的过期时间
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        // 返回nil, 表示加锁成功
        "return nil; " +
        "end; " +
    
        // 如下是可重入锁的逻辑

        // 通过hexists指令判断当前的锁是不是自己的,只有是自己的锁,才支持可重入
        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
        // 通过hincrby指令更新哈希结构的数据,将hash value对应的可重入次数加一
    	"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
        // 通过pexpire重新设置锁的过期时间
        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
        // 返回nil, 表示加锁成功
        "return nil; " +
        "end; " +

        // 如果当前有人获取了锁,并且这个锁不是自己的,那么将会执行pttl指令,返回当前锁剩余的过期时间
        "return redis.call('pttl', KEYS[1]);"

LUA脚本的参数信息说明:

  • KEYS[1]: Collections.singletonList(getRawName()) ==> 我们指定的分布式锁的key
  • ARGV[1]: unit.toMillis(leaseTime) ==> 锁的超时时间
  • ARGV[2]: getLockName(threadId) ==> 每个进程的唯一ID + ":" + 当前线程ID

因为微服务集群部署的时候,线程ID有可能重复,所以需要拼接上每个服务实例的唯一标识,也就是进程唯一ID去保证唯一,我们可以在应用启动的时候,使用uuid生成一个常量,作为每个服务实例的唯一标识。当然也可以使用其它方式去生成,只需要保证每个进程唯一即可。

加锁的逻辑基本上都在LUA脚本里面了,可以看到,当执行LUA脚本返回nil的时候,表示锁获取成功,否则返回的是锁剩余的过期时间,即其他线程需要等待的时间。

加锁后Redis内的数据格式如下:

 即:

  • redis key = lock,这个lock就是我们前面通过redissonClient.getLock("lock")指定的;
  • hash key = 43689446-d9c1-4371-a32b-f9c320f8d803:1,即uuid + ":" + 当前线程ID;
  • hash value = 1,表示当前锁的重入次数为1;

以上主要介绍了 Redisson 可重入锁的加锁、锁重入逻辑,核心都在那一段LUA脚本。

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/127491298