水煮Redisson(十五)-写锁

131 阅读3分钟

前言

读锁是共享的,写锁是排他的,使用与读多写少的业务场景。
写锁的实现【RedissonWriteLock】,继承自非公平锁RedissonLock,主要重写了下面几个方法:

  • tryLockInnerAsync:获取锁
  • unlockInnerAsync:手动解锁
  • forceUnlockAsync:强制解锁
  • isLocked:锁是否被持有
    可以发现写锁比读锁少重写一个方法:renewExpirationAsync,直接使用了父类非公平锁内的默认实现。

获取锁

    @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', 'write'); " +
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                              "end; " +
                              "if (mode == 'write') then " +
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                      "local currentExpire = redis.call('pttl', KEYS[1]); " +
                                      "redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
                                      "return nil; " +
                                  "end; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getName()), 
                        internalLockLeaseTime, getLockName(threadId));
    }

执行步骤:

  1. 如果锁没有被其他客户端持有,则当前客户端线程直接持有锁,并设置mode为write;
  2. 如果资源锁已经被其他客户端持有【写锁】,且持有者不是当前线程,则返回等待时间,获取锁失败;
  3. 如果持有写锁的是当前线程,则累加持有次数,并延长锁超时时间;超时时间根据重入时带上的超时参数累加所得。
    疑问:源码中不判断当前持有写锁的是否为当前线程,如果一个线程先持有读锁,再重入获取写锁,是否会造成写锁被共享的异常情况?

释放锁

手动释放

手动释放的逻辑比读锁简单很多,因为读锁被设定为共享的,释放时,需要考虑其他持有锁的线程,根据其中超时时间最长的一个客户端,来重置锁资源的超时时间。

    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        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;" +
                "if (mode == 'write') then " +
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
                    "if (lockExists == 0) then " +
                        "return nil;" +
                    "else " +
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        "if (counter > 0) then " +
                            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                            "return 0; " +
                        "else " +
                            "redis.call('hdel', KEYS[1], ARGV[3]); " +
                            "if (redis.call('hlen', KEYS[1]) == 1) then " +
                                "redis.call('del', KEYS[1]); " +
                                "redis.call('publish', KEYS[2], ARGV[1]); " + 
                            "else " +
                                // has unlocked read-locks
                                "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                            "end; " +
                            "return 1; "+
                        "end; " +
                    "end; " +
                "end; "
                + "return nil;",
        Arrays.<Object>asList(getName(), getChannelName()), 
        LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

执行步骤:

  1. 判断锁是否存在,如果不存在,则直接发送释放信号,通知其他线程争抢,返回true;
  2. 如果锁的模式不是write,直接返回false;
  3. 如果锁的模式是write,判断当前线程是否在持有列表中,如果当前线程没有持有记录,则直接返回false;否则继续往下执行;
  4. 递减当前线程的持有次数,如果持有次数不为零,则重置锁的过期时间,返回false;
  5. 如果当前线程递减次数之后为零,则删除客户端线程的持有记录;
  6. 如果此时资源锁中没有其他持有者,则删除资源锁,并通知其他线程争抢,否则将资源锁的mode切换为read,返回true;
    疑问:与获取中的问题一样,释放时不能确定写锁持有者是否为当前线程,是否会误操作?

强制释放

强制释放逻辑和读锁中的基本一致,读锁中判断模式是否为read,这里判断是否为write,如果是write,表示锁已经被持有,且为写锁。操作逻辑比较简单,直接删除锁资源,并发布锁释放的消息,提示其他客户端线程订阅者进行争抢锁。

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

是否被持有

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

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

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