Redisson分布式锁学习总结:写锁 RedissonWriteLock#lock 获取锁源码分析

1、RedissonWriteLock 之 lua 脚本加锁

上两篇文章,我们已经分析了读锁 RedissonReadLock 的加锁和释放锁的执行原理。下面,我们直入主题,将先分析写锁 RedissonWriteLock 的加锁原理,至于 watchdog 机制中的 lua 脚本,RedissonWriteLock 和 RedissonLock 保持一致,不需和 RedissonReadLock 一样单独分析。

RedissonWriteLock#tryLockInnerAsync:

@Override
<T> RFuture<T> tryLockInnerAsync(long waitTime, 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));
}

我们可以看到,写锁获取锁的 lua 脚本不长,我们一步一步分析。

分析前,我们先定好,读写锁的key为 myLock:

RedissonClient client = RedissonClientUtil.getClient("");
RReadWriteLock readWriteLock = client.getReadWriteLock("myLock");

1.1、KEYS

Arrays.asList(getName()):

  • getName(): 锁key

KEYS:[“myLock”]

1.2、ARGVS

internalLockLeaseTime, getLockName(threadId):

  • internalLockLeaseTime:其实就是 watchdog 的超时时间,默认是30000毫秒,可看 Config#lockWatchdogTimeout。
  • getLockName(threadId):super.getLockName(threadId) + “:write” -> 客户端ID(UUID):线程ID(threadId):write

ARGVS:[30_000毫秒,“UUID:threadId:write”]

1.3、lua 脚本分析

1、第一步,获取锁模式

lua脚本

local mode = redis.call('hget', KEYS[1], 'mode'); 

分析:

  1. 利用 hget 命令获取当前锁模式
    hget myLock mode
    

2、分支一:锁的模式为空,即当前锁尚未被其他线程持有

场景:

  • 当前线程尝试获取写锁时,还没有其他线程成功持有锁,包括读锁和写锁

lua脚本:

"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; " +

分析:

  1. 利用 hset 命令设置锁模式为写锁

    hset myLock mode write
    

    执行后,锁内容如下:

    myLock:{
        "mode":"write"
    }
    
  2. 利用 hset 命令为当前线程添加加锁次数记录

    hset myLock UUID:threadId:write 1
    

    执行后,锁的内容如下:

    myLock:{
        "mode":"write",
        "UUID:threadId:write":1
    }
    

    我们可以发现,读写锁中的写锁获取锁不再需要写锁中的加锁超时记录,因为写锁仅支持一个线程来持有锁,锁的超时时间就是线程持有锁的超时时间。

  3. 利用 pexpire 命令为锁添加过期时间

    pexpire myLock 30000
    
  4. 最后返回nil,表示获取锁成功

3、分支二:锁模式为写锁并且持有写锁为当前线程,当前线程可再次获取写锁

场景:

  • 当前线程重复获取写锁,即读写锁中的写锁支持可重入获取锁

lua脚本:

"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;"

分析:

  1. 锁模式为写锁,利用 hexists 命令判断持有写锁为当前线程

    hexists myLock UUID:threadId:write
    
  2. 利用 hincrby 命令为当前线程增加1次加锁次数

    hincrby myLock UUID:threadId:write 1
    

    假设之前当前线程获取1次写锁,那么执行后,redis里写锁的相关数据:

    
    myLock:{
        "mode":"write",
        "UUID:threadId:write":2
    }
    

    我们可以看到,读写锁里面写锁在 redis 里面的数据,和 RedissonLock 相比,只多了一个mode字段来标识当前读写锁的模式;当然了,写锁也支持相同线程可重入获取锁。

  3. 利用 pttl 获取当前写锁的超时剩余毫秒数

    pttl myLock
    
  4. 利用 pexipre 给锁重新设置锁的过期时间,过期时间为:上次加锁的剩余毫秒数+30000毫秒

    pexpire myLock currentExpire+30000
    
  5. 最后返回nil,表示获取锁成功

4、最后:获取锁失败,返回锁pttl

场景:

  • 不满足上面的两个分支,当前线程就无法成功获取写锁

lua脚本:

"return redis.call('pttl', KEYS[1]);"

分析:

  1. 利用 pttl 命令获取锁过期时间(毫秒)

    pttl myLock
    
  2. 直接返回步骤1的获取到的毫秒数

2、后续

因为 RedissonWriteLock 也是基于 RedissonLock 扩展的,所以关于 watchdog 和获取锁失败等机制,就不再详述了,和 RedissonLock 基本保持一致。

你可能感兴趣的:(分布式锁系列,分布式锁,redisson,读写锁)