Redisson 分布式锁之 RReadWriteLock

Redisson 分布式锁之 RReadWriteLock


基本使用

				
		String READ_WRITE_LOCK_KEY = "read_write_lock"
		//获取key为 read_write_lock 的锁对象
        RReadWriteLock rReadWriteLock = redisson.getReadWriteLock(READ_WRITE_LOCK_KEY);
        //获取读锁 或者是 rReadWriteLock.writeLock(); 获取写锁
		RLock rLock = rReadWriteLock.readLock(); 
		//常规try finally加解锁操作
        rLock.lock( );
        try {
            //业务操作逻辑
          	......
        } finally {
            rLock.unlock();
        }

读写锁特性:

所谓读写锁多个线程可以同时持有读锁,但只能有一个线程持有写锁,当写锁被持有时,其他线程无法获取读锁或写锁。

RReadWriteLock 源码分析

public interface RReadWriteLock extends ReadWriteLock {

    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    @Override
    RLock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    @Override
    RLock writeLock();

}

首先看一下RReadWriteLock这个接口,接口中定义了两个方法,然后查看相关类图可以得知是由RedissonReadWriteLock来实现了这个接口

在这里插入图片描述

查看RedissonReadWriteLock源码可以得知获取的读锁实际上是RedissonReadLock这个对象,写锁实际上RedissonWriteLock

public class RedissonReadWriteLock extends RedissonExpirable implements RReadWriteLock {

    public RedissonReadWriteLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }

    @Override
    public RLock readLock() {
        return new RedissonReadLock(commandExecutor, getName());
    }

    @Override
    public RLock writeLock() {
        return new RedissonWriteLock(commandExecutor, getName());
    }

}

RedissonReadLock 解析

翻看RedissonReadLock源码可以发现这个类继承了RedissonLock,因此只需要关注它重写的方法即可,这里只贴出了核心的加解锁方法

public class RedissonReadLock extends RedissonLock implements RLock {
  
  ......
    
    //读锁加锁方法
    @Override
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return commandExecutor.syncedEval(getRawName(), 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(getRawName(), getReadWriteTimeoutNamePrefix(threadId)),
                        unit.toMillis(leaseTime), getLockName(threadId), getWriteLockName(threadId));
    }

  	//读锁解锁方法
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);

        return commandExecutor.syncedEval(getRawName(), 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(getRawName(), getChannelName(), timeoutPrefix, keyPrefix),
                LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
    }
    
  ......
  
}

单独看一下读锁的加锁方法

		//读锁加锁方法
    @Override
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
                                // 获取锁模式 hget mode
                                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                // 如果没获取到,及当前未有线程持有锁
                                "if (mode == false) then " +
                                  // 设置类型为hset,key为 read_write_lock,设置mode为read,即锁模式为读锁
                                  // hest read_write_lock mode read
                                  "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                                  // 设置当前线程加锁次数为 1        
                                  // hest read_write_lock UUID:ThreadId 1
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  // 为获取到锁的当前线程设置一条记录,类型为string 
                                  // set {read_write_lock }:UUID:threadId:rwlock_timeout:1 1      
                                  "redis.call('set', KEYS[2] .. ':1', 1); " +
                                  // 为这条记录设置超时时间
                                  // pexpire {read_write_lock }:UUID:threadId::rwlock_timeout:1 30000      
                                  "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
                                  // pexpire read_write_lock 30000     
                                  "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 " +
                                  // 设置当前线程加锁次数 +1
                                  "local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                  // key = {read_write_lock }:UUID:threadId:rwlock_timeout:ind(重入次数)
                                  "local key = KEYS[2] .. ':' .. ind;" +
                                  // 为获取到锁的当前线程设置一条记录,类型为string 
                                  // set {read_write_lock }:UUID:threadId:rwlock_timeout:ind 1
                                  "redis.call('set', key, 1); " +
                                  // 为这条记录设置超时时间
                                  // pexpire {read_write_lock }:UUID:threadId::rwlock_timeout:ind 30000   
                                  "redis.call('pexpire', key, ARGV[1]); " +
                                  // 获取 read_write_lock 锁过期时间
                                  "local remainTime = redis.call('pttl', KEYS[1]); " +
                                  // 当前过期时间和 30000 获取最大值为 read_write_lock 重新设置过期时间
                                  "redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1])); " +
                                  "return nil; " +
                                "end;" +
                                // 获取 read_write_lock 过期时间
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getRawName(), getReadWriteTimeoutNamePrefix(threadId)),
                        unit.toMillis(leaseTime), getLockName(threadId), getWriteLockName(threadId));
    }

参数说明:

KEYS = Arrays.asList(getRawName(), getReadWriteTimeoutNamePrefix(threadId))

KEYS[1] = getRawName() //即获取锁对象时设置的 “read_write_lock”

KEYS[2] = getReadWriteTimeoutNamePrefix(threadId) // 即 {read_write_lock }:UUID:threadId:rwlock_timeout

ARGV = unit.toMillis(leaseTime), getLockName(threadId), getWriteLockName(threadId)

ARGV[1] = unit.toMillis(leaseTime) // 锁超时时间,默认30s

ARGV[2] = getLockName(threadId) // 读锁名称,UUID:ThreadId

ARGV[3] = getWriteLockName(threadId) // 写锁名称,UUID:threadId:write

加读锁

首次加读锁,先执行lua脚本的第一个if分支,观察一下redis中的数据,其中一个有一个key为read_write_lock结构为hash的锁,包含锁的模式、加锁的线程重入的次数;另一个key为{read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1结构为string的数据,主要记录的是当前加锁线程的超时时间。

# 获取当前db下所有key,可以看到生成两个值,一个是hash的read_write_lock,另一个是string的
# {read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1
127.0.0.1:6379[3]> keys *
1) "read_write_lock"
2) "{read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1"
127.0.0.1:6379[3]> type read_write_lock
hash
127.0.0.1:6379[3]> type {read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1
string
# 查看 read_write_lock,锁模式为 read,7b8aefd7-e765-4788-abb4-e4c40264eda4:76 重入次数为 1
127.0.0.1:6379[3]> hgetall read_write_lock
1) "mode"
2) "read"
3) "7b8aefd7-e765-4788-abb4-e4c40264eda4:76"
4) "1"
127.0.0.1:6379[3]> get {read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1
"1"
# 锁过期时间默认30s
127.0.0.1:6379[3]> ttl read_write_lock
(integer) 25
127.0.0.1:6379[3]> ttl {read_write_lock}:7b8aefd7-e765-4788-abb4-e4c40264eda4:76:rwlock_timeout:1
(integer) 24

读锁的重入

接下来看一下读锁的重入,修改一下调用代码,重新执行得到读读重入的数据,此时可以发现有两条string命令,lua脚本中重入的时候会先获取ind的值 ind = redis.call('hincrby', KEYS[1], ARGV[2], 1)ind为重入次数2,会利用ind重新设置key的值key = KEYS[2] .. ':' .. ind,因此会看到两个string类型的值,当一个线程重入n次,就会生成n个string类型的值。

# 获取当前db下所有key,可以看到生成三个值,一个是hash的read_write_lock,另两个是string类型的,记录当前加
# 锁线程的过期时间
127.0.0.1:6379[3]> keys *
1) "{read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:1"
2) "read_write_lock"
3) "{read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:2"
# read_write_lock 锁模式为 read,重入次数变成了 2
127.0.0.1:6379[3]> hgetall read_write_lock
1) "mode"
2) "read"
3) "6a603eb7-b74f-4006-95c9-29d8210c8a24:72"
4) "2"
127.0.0.1:6379[3]> get {read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:1
"1"
127.0.0.1:6379[3]> get {read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:2
"1"
127.0.0.1:6379[3]> ttl {read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:2
(integer) 27
127.0.0.1:6379[3]> ttl {read_write_lock}:6a603eb7-b74f-4006-95c9-29d8210c8a24:72:rwlock_timeout:1
(integer) 21
127.0.0.1:6379[3]> ttl read_write_lock
(integer) 24

观察判断锁模式的时候,如果是模式是读正常重入或者是模式为写,read_write_lockUUID:threadId:write为当前写锁是也可以正常重入,即当前线程也可以获得读锁,Redisson提供的读写锁并不是完全互斥的,对于同一线程而言是可以获取写锁后再获取读锁的,也称为锁的降级。

# 获取当前db下所有key,可以看到生成两个值,一个是hash的read_write_lock,另一个是string类型的,记录当前加
# 锁线程的过期时间
127.0.0.1:6379[3]> keys *
1) "{read_write_lock}:1d2e6ae7-4cd1-48fc-bd6d-a90e1afd86f6:72:rwlock_timeout:1"
2) "read_write_lock"
# read_write_lock 锁模式为 write
127.0.0.1:6379[3]> hgetall read_write_lock
1) "mode"
2) "write"
3) "1d2e6ae7-4cd1-48fc-bd6d-a90e1afd86f6:72:write"
4) "1"
5) "1d2e6ae7-4cd1-48fc-bd6d-a90e1afd86f6:72"
6) "1"

对于同一线程先写后读,string类型的key是redis.call('set', key, 1)生成的这个key是{read_write_lock }:UUID:threadId:rwlock_timeout是读锁生成的,这个key默认时间过后就会失效,具体原因在后面watch dog续期机制中有讲。

(这里可以提前知道,针对读锁的加锁次数记录使用的是UUID:ThreadId,写锁的加锁记录使用的是UUID:ThreadId:write

读读操作

现在有两个客户端均执行读操作,修改代码执行后得到

# 获取当前db下所有key,可以看到生成三个值,一个是hash的read_write_lock,另两个是string类型的,记录当前加
# 锁线程的过期时间
127.0.0.1:6379[3]> keys *
1) "{read_write_lock}:459dcfda-b774-4ad3-acf0-fdb9b66fb3af:72:rwlock_timeout:1"
2) "read_write_lock"
3) "{read_write_lock}:70e2094d-8765-4aa4-adcd-e91f6f815e37:72:rwlock_timeout:1"
127.0.0.1:6379[3]> hgetall read_write_lock
1) "mode"
2) "read"
3) "70e2094d-8765-4aa4-adcd-e91f6f815e37:72"
4) "1"
5) "459dcfda-b774-4ad3-acf0-fdb9b66fb3af:72"
6) "1"

hash类型中记录了有两个线程都获取到了读锁,并且都实用string记录了线程的超时时间

读写互斥

如果已经存在读锁,此时这个读锁属于A线程,B线程请求读锁第一第二个判断条件都不满足因此会直接返回当前锁的过期时间,并且订阅消息通道redisson_rwlock:{read_write_lock},自旋等待读锁释放

回顾读锁加锁

  • {read_write_lock}是hash类型的,里面存放的有锁模式(mode=read是读锁、mode=write是写锁)另一个存放的是锁重入次数,会对{read_write_lock}设置当前线程读锁的重入次数,用的字段是UUID:ThreadId
  • 每次加锁都会维护一个key用来表示这个锁的过期时间,数据类型是string,key的名称是{read_write_lock }:UUID:threadId:rwlock_timeout:重入次数

再看一下读锁解锁过程

    //读锁解锁方法
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);

        return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
              	// 获取锁模式 hget mode
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                // 如果锁模式为空,对 redisson_rwlock:{read_write_lock} 通道发布释放锁的消息
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end; " +
                // 判断当前线程是否持有读锁
                // hexists read_write_lock UUID:ThreadId
                "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
                "if (lockExists == 0) then " +
                    "return nil;" +
                "end; " +
                // 持有锁,对锁重入次数减1
                // hincrby read_write_lock UUID:ThreadId -1
                "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + 
                // 如果当前线程重入次数为0,删除当前线程中的加锁记录
                "if (counter == 0) then " +
                    // hdel read_write_lock UUID:ThreadId
                    "redis.call('hdel', KEYS[1], ARGV[2]); " + 
                "end;" +
                 // 删除当前线程对应的锁超时记录
                 // del {read_write_lock }:UUID:threadId:rwlock_timeout:(counter+1)
                "redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
                // 获取 read_write_lock 中元素个数
                "if (redis.call('hlen', KEYS[1]) > 1) then " +
                    "local maxRemainTime = -3; " + 
                    // 获取 read_write_lock 中所有的key
                    "local keys = redis.call('hkeys', KEYS[1]); " + 
                    // 遍历key 实际上后续逻辑中只处理 read_write_lock 中值为重入次数的 key                     
                    "for n, key in ipairs(keys) do " + 
                        // 这两步就是去除 key 为 mode 的数据
                        "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                        "if type(counter) == 'number' then " + 
                            // 由于每次重入都会记录一条当前锁的超时记录,加锁的时候使用 ind 来进行重入次数	  
                            // 计算,这个地方倒序获取每个记录的超时时间            
                            "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; " +
                     // 判断 maxRemainTime 是否大于0,如果大于0,给锁重新设置过期时间,结束执行
                    "if maxRemainTime > 0 then " +
                        "redis.call('pexpire', KEYS[1], maxRemainTime); " +
                        "return 0; " +
                    "end;" + 
                    // 如果锁模式为 write 返回   
                    "if mode == 'write' then " + 
                        "return 0;" + 
                    "end; " +
                "end; " +
                // 如果走到这步,说明当前线程解锁后,这个读写锁没有任何线程持有了,这时把读写锁删除
                // 并且往对应的 channel 中发送解锁的消息
                // del read_write_lock
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; ",
                Arrays.<Object>asList(getRawName(), getChannelName(), timeoutPrefix, keyPrefix),
                LockPubSub.UNLOCK_MESSAGE, getLockName(threadId));
    }

参数说明:

KEYS = Arrays.asList(getRawName(), getChannelName(), timeoutPrefix, keyPrefix)

KEYS[1] = getRawName() //即获取锁对象时设置的 “read_write_lock”

KEYS[2] = getChannelName() // 订阅消息的通道,redisson_rwlock:{read_write_lock}

KEYS[3] = timeoutPrefix // 即 {read_write_lock }:UUID:threadId:rwlock_timeout

KEYS[4] = keyPrefix // 即 {read_write_lock }

ARGV = LockPubSub.UNLOCK_MESSAGE, getLockName(threadId)

ARGV[1] = LockPubSub.UNLOCK_MESSAGE // Redis发布事件的message 0L

ARGV[2] = getLockName(threadId) // 读锁名称,UUID:ThreadId

RedissonWriteLock 解析

依旧是查看RedissonWriteLock的源码,这里也只贴出重写的核心方法加锁和解锁

public class RedissonWriteLock extends RedissonLock implements RLock {
  
	......
    
  // 加锁方法
  @Override
  <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return commandExecutor.syncedEval(getRawName(), 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(getRawName()),
                        unit.toMillis(leaseTime), getLockName(threadId));
    }
  
  	// 解锁方法
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), 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(getRawName(), getChannelName()),
        LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }
  
  ......
    
}

首先看一下加锁方法

@Override
  <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
                            // 依旧是先获取锁模式              
                            "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                            // 这步的意思就是如果不存在 read_write_lock 这个锁,那么当前线程就抢占到写锁
                            "if (mode == false) then " +
                                  // 设置模式为 writre
                                  // hset read_write_lock mode write
                                  "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                                  // 设置当前线程的重入次数        
                                  // hset read_write_lock UUID:ThreadId:write 1
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  // 设置过期时间
                                  // expire read_write_lock 30000
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                              "end; " +
                              // 如果模式是 write
                              "if (mode == 'write') then " +
                                  // 获得锁线程是当前线程
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                      // 锁重入次数加一
                                      // hincrby read_write_lock  UUID:ThreadId:write 1
                                      "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;" +
                                // 都不满足的话返回当前 read_write_lock 的过期时间
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getRawName()),
                        unit.toMillis(leaseTime), getLockName(threadId));
    }


参数说明:

KEYS = Arrays.asList(getRawName())

KEYS[1] = getRawName() // 即获取锁对象时设置的 “read_write_lock”

ARGV = unit.toMillis(leaseTime), getLockName(threadId))

ARGV[1] = unit.toMillis(leaseTime) // 默认30s

ARGV[2] = getLockName(threadId) // 写锁名称,UUID:ThreadId:write

回顾写锁加锁

写锁的添加依旧是两块判断逻辑,其中一块是如果当前没有线程持有锁,正常加锁,但是可以发现写锁加锁流程跟读锁不一样。写锁的名称是UUID:ThreadId:write,读锁是UUID:ThreadId;写锁加锁时不会单独创建一个记录当前锁过期时间的信心,这也是为什么在读锁的重入中先获取写锁在获取读锁的情况下,只会有一个string类型的数据。另一块逻辑是如果当前锁模式为write且获取锁的线程就是当前线程,可以正常重入,read_write_lockUUID:ThreadId:write的值加一。如果两个条件都不满足那么返回当前锁的过期时间,并自旋等待获取锁。

再看一下写锁解锁方法

    // 解锁方法
    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                // 首先依旧是先获取 mode 值
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                // 如果锁模式为空,对 redisson_rwlock:{read_write_lock} 通道发布释放锁的消息
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                // 锁模式为 write
                "if (mode == 'write') then " +
                    // 判断是否是当前线程持有的锁
                    // hexists read_write_lock UUID:ThreadId:write
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
                    // 如果不是直接返回          
                    "if (lockExists == 0) then " +
                        "return nil;" +
                    "else " +
                        // 对当前锁的重入次数减 1 并获取返回值
                        // hincrby read_write_lock UUID:ThreadId:write -1
                        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                        // 如果次数不为 0 重新设置过期时间 返回
                        "if (counter > 0) then " +
                            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                            "return 0; " +
                        "else " +
                            // 如果当前重入次数为 0 删除当前 field
                            // hdel read_write_lock UUID:ThreadId:write
                            "redis.call('hdel', KEYS[1], ARGV[3]); " +
                            // 如果 read_write_lock 的长度只为 1 即只剩下 filed 为 mode 的
                            // 直接删除 read_write_lock 这个 key
                            "if (redis.call('hlen', KEYS[1]) == 1) then " +
                                "redis.call('del', KEYS[1]); " +
                             		// 并且往对应的 channel 中发送解锁的消息
                                "redis.call('publish', KEYS[2], ARGV[1]); " + 
                            "else " +
                                // has unlocked read-locks
                              	// 实际上这步就是 锁降级的时候会出现此情况,当前线程持有写锁,同一线程
                             		// 读锁进来后依旧能成功上锁,这个时候写锁解锁了,读锁没解锁的话把 mode 设置
                              	// 为 read
                                "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                            "end; " +
                            "return 1; "+
                        "end; " +
                    "end; " +
                "end; "
                + "return nil;",
        Arrays.<Object>asList(getRawName(), getChannelName()),
        LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

参数说明:

KEYS = Arrays.asList(getRawName(), getChannelName())

KEYS[1] = getRawName() // 即获取锁对象时设置的 “read_write_lock”

KEYS[1] = getChannelName() // 订阅消息的通道,redisson_rwlock:{read_write_lock}

ARGV = LockPubSub.READ_UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId))

ARGV[1] = LockPubSub.READ_UNLOCK_MESSAGE // Redis发布事件的message 0L

ARGV[2] = internalLockLeaseTime // 默认 30 s

ARGV[3] = getLockName(threadId) // 写锁名称,UUID:ThreadId:write

回顾写锁解锁

实际上写锁解锁的时候需要主要是否存在锁降级的过程,可以看一下上文中读锁重入的地方,这时即使把read_write_lock中的写锁的field删除后,还存在一个读锁的field,因此会把当前read_write_lock锁模式设置为read

# read_write_lock 锁模式为 write
127.0.0.1:6379[3]> hgetall read_write_lock
1) "mode"
2) "read"
3) "1d2e6ae7-4cd1-48fc-bd6d-a90e1afd86f6:72"
4) "1"

锁续期问题

这里只贴出了RedissonReadLock的锁续期方法

@Override
    protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
        
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                // 获取 read_write_lock UUID:ThreadId 的值,也就是获取读锁的重入次数
                // hget read_write_lock UUID:ThreadId
                "local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
                "if (counter ~= false) then " +
                    // 对 read_write_lock 重新设置过期时间 默认 30s
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    // 如果 read_write_lock 里面 field 个数大于两个 获取所有 key
                    "if (redis.call('hlen', KEYS[1]) > 1) then " +                              
                        "local keys = redis.call('hkeys', KEYS[1]); " + 
                        // 对 read_write_lock 里的 key 进行遍历
                        "for n, key in ipairs(keys) do " + 
                            // 依旧是取值为重入次数的 key
                            "counter = tonumber(redis.call('hget', KEYS[1], key)); " + 
                            "if type(counter) == 'number' then " + 
                                // 倒序遍历
                                "for i=counter, 1, -1 do " + 
                                    // 重新设置过期时间
                                    // pexpire {read_write_lock }:UUID:ThreadId:rwlock_timeout:i 30000
                                    "redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " + 
                                "end; " + 
                            "end; " + 
                        "end; " +
                    "end; " +
                    
                    "return 1; " +
                "end; " +
                "return 0;",
            Arrays.<Object>asList(getRawName(), keyPrefix),
            internalLockLeaseTime, getLockName(threadId));
    }

参数说明:

KEYS = Arrays.asList(getRawName(), keyPrefix)

KEYS[1] = getRawName() // 即获取锁对象时设置的 “read_write_lock”

KEYS[2] = keyPrefix() // 即 {read_write_lock}

ARGV = internalLockLeaseTime, getLockName(threadId)

ARGV[1] = internalLockLeaseTime // 默认 30 s

ARGV[2] = getLockName(threadId) // 读锁名称,UUID:ThreadId

上述lua脚本中有一个hlen的判断实际上就是为获取read_write_lock中排除锁模式的其他所有field

获取当前field中的值(redis.call('hget', KEYS[1], key)),把获取的值转化为数字倒序进行遍历对每一个{read_write_lock }:UUID:ThreadId:rwlock_timeout:i重新设置过期时间,实际上就是这个线程的每一个加锁记录重新设置过期时间

总结

  • 为什么只有锁降级没有锁升级

锁降级指的是同一线程先获取写锁,然后再获取读锁,这样可以保证数据的一致性和正确性,在一定程度上减少锁的竞争和消耗。锁降级的过程是安全的,因为在获取读锁之前,已经获取了写锁,保证了共享资源的独占性和一致性。

锁升级指的是先获取读锁,然后再尝试获取写锁,这样会导致死锁和数据不一致的问题。因为在多个线程同时持有读锁的情况下,如果有一个线程尝试获取写锁,则会阻塞其他读锁和写锁,导致死锁。而且,在多个线程同时持有读锁的情况下,不能保证共享资源的一致性和正确性,因为读锁允许多个线程同时访问共享资源,而写锁则需要独占共享资源,这两种操作是不兼容的。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值