暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

不送花的程序猿 2021-11-29
446


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.<Object>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

  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 基本保持一致。

文章转载自不送花的程序猿,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论