水煮Redisson(十四)-读锁

240 阅读6分钟

前言

读锁是共享的,写锁是排他的。
读锁的实现RedissonReadLock,继承自非公平锁RedissonLock,主要重写了下面几个方法:

  • tryLockInnerAsync:获取锁
  • unlockInnerAsync:手动解锁
  • renewExpirationAsync:重置过期时间
  • forceUnlockAsync:强制解锁
  • isLocked:锁是否被持有

下面逐个分析

获取锁

简介:如果锁不存在,则直接持有;如果锁为读模式,或者当前线程持有写锁,则当前线程可以直接成功获取锁。
锁的信息和非公平锁一样,存放在hash结构中,包含属性:mode【读或者写】,

    @Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                "if (mode == false) then " +
                                  "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  "redis.call('set', KEYS[2] .. ':1', 1); " +
                                  "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end; " +
                                "if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
                                  "local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                  "local key = KEYS[2] .. ':' .. ind;" +
                                  "redis.call('set', key, 1); " +
                                  "redis.call('pexpire', key, ARGV[1]); " +
                                  "local remainTime = redis.call('pttl', KEYS[1]); " +
                                  "redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1])); " +
                                  "return nil; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)), 
                        internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId));
    }

执行步骤:

  1. 获取当前锁的模式【mode】,如果不存在,则拿锁成功,写入当前客户端线程锁信息:mode,持有锁次数,设置锁过期时间;
  2. 为当前客户端线程设置一个缓存,和当前获取的锁过期时间一致;返回true;
  3. 如果mode是读,或者【mode是写,并且持有写锁的线程是当前客户端】,则累加锁的持有次数,用当前线程id和持有次数组合成一个key,设定过期时间;
  4. 获取当前key的剩余过期时间,取剩余时间和方法参数中过期时间比较大的一个值,重置缓存过期时间;
  5. 如果前面两个分支都不满足条件,则返回过期时间;

解锁

手动解锁

简介:

    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end; " +
                "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
                "if (lockExists == 0) then " +
                    "return nil;" +
                "end; " +
                    
                "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + 
                "if (counter == 0) then " +
                    "redis.call('hdel', KEYS[1], ARGV[2]); " + 
                "end;" +
                "redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
                
                "if (redis.call('hlen', KEYS[1]) > 1) then " +
                    "local maxRemainTime = -3; " + 
                    "local keys = redis.call('hkeys', KEYS[1]); " + 
                    "for n, key in ipairs(keys) do " + 
                        "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                        "if type(counter) == 'number' then " + 
                            "for i=counter, 1, -1 do " + 
                                "local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " + 
                                "maxRemainTime = math.max(remainTime, maxRemainTime);" + 
                            "end; " + 
                        "end; " + 
                    "end; " +
                            
                    "if maxRemainTime > 0 then " +
                        "redis.call('pexpire', KEYS[1], maxRemainTime); " +
                        "return 0; " +
                    "end;" + 
                        
                    "if mode == 'write' then " + 
                        "return 0;" + 
                    "end; " +
                "end; " +
                    
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; ",
                Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix), 
                LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
    }

执行步骤:

  1. 获取锁的模式,如果锁不存在,则通知其他客户端线程进行尝试拿锁;如果锁存在,执行后续逻辑;
  2. 判断持有锁的是否为当前线程,如果不是,则直接返回false;
  3. 读取当前线程减一之后剩余的持有次数,如果剩余次数为零,则删除当前线程的锁持有凭证;
  4. 删除锁超时组合key,当前线程id + 持有次数;redisson读锁对每一次持有都记录超时时间;
  5. 如果资源锁中的属性大于一【表示有其他线程持有当前资源锁】,定义锁衰减时间,获取锁hash中所有的键值对【持有当前锁的客户端线程】,遍历;如果有持有次数,则取出每次锁的过期时间,通过计算,获取最大的过期时间
  6. 如果最大过期时间大于零,则设置此锁资源的过期时间为此值,并返回失败;如果小于零,则继续往下执行;
  7. 如果锁模式为写,则返回解锁失败;跳出【第5步】的IF分支,继续往下执行;解锁失败是因为资源锁中的属性大于一,且模式为write,表示还有其他线程持有了当前资源的写锁;写锁是不可共享的,说明当前客户端线程是读锁,不能释放资源的独占锁。
  8. 如果资源锁HASH中没有其他客户端线程持有锁,则删除锁资源,并发布锁释放的信号,通知其他排队线程进行争抢,返回成功;

强制解锁

简介:强制解锁,只能是读锁的模式下,如果当前锁模式为写锁,则强制解锁失败。

    @Override
    public RFuture<Boolean> forceUnlockAsync() {
        cancelExpirationRenewal(null);
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hget', KEYS[1], 'mode') == 'read') then " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end; " +
                "return 0; ",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE);
    }

执行步骤:

  1. 判断当前锁资源是否为读锁模式,如果是,则删除锁资源,并发布锁释放的信号,通知其他排队线程进行争抢,返回成功;
  2. 如果当前锁资源是写锁模式,则直接返回失败。
    这里有个疑问:如果模式为写锁,而且执行强制释放的是持有的客户端线程,也不能强制释放吗?毕竟持有者应该是有资格进行锁释放的。
    猜测:强制解锁是没有参数传入的,不知道执行者的身份;所以不能判定当前写锁的持有者是否为当前客户端线程;

重置过期时间

执行的契机是当前客户端线程成功获取锁之后,重置其超时时间为最初调用tryLock(timeout)方法时,传入的timeout值【默认为看门狗时间:30秒】。

    @Override
    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
        
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
                "if (counter ~= false) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    
                    "if (redis.call('hlen', KEYS[1]) > 1) then " +
                        "local keys = redis.call('hkeys', KEYS[1]); " + 
                        "for n, key in ipairs(keys) do " + 
                            "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                            "if type(counter) == 'number' then " + 
                                "for i=counter, 1, -1 do " + 
                                    "redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " + 
                                "end; " + 
                            "end; " + 
                        "end; " +
                    "end; " +
                    
                    "return 1; " +
                "end; " +
                "return 0;",
            Arrays.<Object>asList(getName(), keyPrefix), 
            internalLockLeaseTime, getLockName(threadId));
    }

执行步骤:

  1. 获取当前线程持有锁的次数,如果没有持有,则直接返回false;
  2. 重置资源锁的超时时间为当前客户端调用tryLock(waitTime,leaseTime)方法时,传入的leaseTime值;问题:读锁是共享的,资源锁的超时时间会被最后一个持有的客户端线程重置,线程安全吗,合理吗?
  3. 如果资源所被多个客户端线程持有,则更新每个客户端每次持有记录的超时时间为leaseTime的值。

是否被持有

这个方法也被重写,在非公平锁中,只用判断锁资源在redis中是否存在【isExists()】。

    @Override
    public boolean isLocked() {
        RFuture<String> future = commandExecutor.writeAsync(getName(), StringCodec.INSTANCE, RedisCommands.HGET, getName(), "mode");
        String res = get(future);
        return "read".equals(res);
    }

执行逻辑:获取资源锁的模式,判断模式是否为【read】。