Redis系列-生产应用篇-分布式锁(4)-单进程Redis分布式锁的Java实现(Redisson使用与底层实现)-读写锁

Redisson单进程Redis分布式悲观锁的使用与实现

本文基于Redisson 3.7.5

3. 读写锁

Redisson的分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写锁。写锁是排它锁,获取写锁的时候不能有已经获取读锁和写锁的,获取写锁后,除了本线程以外没发获取读写锁。

RReadWriteLock rwlock = redisson.getLock("testLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

// 支持过期解锁功能
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

查看redisson.getLock("testLock");的源码:

@Override
public RReadWriteLock getReadWriteLock(String name) {
    return new RedissonReadWriteLock(connectionManager.getCommandExecutor(), name);
}

可以看出读写锁的实现类是RReadWriteLock,查看RReadWriteLock的源码:

public class RedissonReadWriteLock extends RedissonExpirable implements RReadWriteLock {
    public RedissonReadWriteLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }
    /**
     * @return 读锁
     */
    @Override
    public RLock readLock() {
        return new RedissonReadLock(commandExecutor, getName());
    }
    /**
     * @return 写锁
     */
    @Override
    public RLock writeLock() {
        return new RedissonWriteLock(commandExecutor, getName());
    }
}

3.1. 读写锁实现思路

首先说一下读写锁的特性:

场景1

  1. 线程A获取了读锁
  2. 线程B尝试获取读锁,获取成功
  3. 线程C尝试获取写锁,获取失败

场景2

  1. 线程A获取了写锁
  2. 线程B尝试获取读锁,获取失败
  3. 线程C尝试获取写锁,获取失败

场景3
1. 线程A获取了读锁
2. 线程A尝试获取写锁,获取失败

场景4
1. 线程A获取了写锁
2. 线程A尝试获取读锁,获取成功

场景5
1. 线程A获取了写锁
2. 线程A再次尝试获取写锁,获取成功
3. 线程A尝试获取读锁,获取成功
4. 线程A再次尝试获取读锁,获取成功
5. 线程A释放读锁,线程A还是持有读锁
6. 线程A释放写锁,线程A还是持有写锁
7. 线程A释放写锁,线程A不再持有写锁
8. 线程B尝试获取读锁,获取成功

设计的基于Redis实现的分布式读写锁,需要满足以上五个场景。
看一下在Redis中,读写锁的分布是:
image

3.2. 读写锁源码分析

3.2.1. 读锁源码

public class RedissonReadLock extends RedissonLock implements RLock {

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

    @Override
    String getChannelName() {
        return prefixName("redisson_rwlock", getName());
    }

    String getWriteLockName(long threadId) {
        return super.getLockName(threadId) + ":write";
    }

    String getReadWriteTimeoutNamePrefix(long threadId) {
        return suffixName(getName(), getLockName(threadId)) + ":rwlock_timeout";
    }

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

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                                //获取当前锁的mode的value
                                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                //如果mode不存在,证明锁还没被占用,直接获取锁
                                "if (mode == false) then " +
                                  //设置HASH的mode为read
                                  "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                                  //设置HASH的threadId对应的LockName的value为1(这个1代表重入次数),相当于该thread获取到了锁
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  //设置threadId对应的tReadWriteTimeoutName末尾连着获取锁次数(因为现在是第一次所以是1)这个key为1
                                  "redis.call('set', KEYS[2] .. ':1', 1); " +
                                  //设置两个key的过期时间为锁过期时间
                                  "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end; " +
                                //如果mode已存在,是read(代表是读锁),或者是write并且获取这个锁的就是本线程
                                "if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
                                  //使HASH的threadId对应的LockName的value+1,代表重入锁获取次数加一
                                  "local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                                  //同时设置threadId对应的tReadWriteTimeoutName末尾连着获取锁次数为1
                                  "local key = KEYS[2] .. ':' .. ind;" +
                                  "redis.call('set', key, 1); " +
                                  //刷新以上两个key的过期时间
                                  "redis.call('pexpire', key, ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)), 
                        internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId));
    }

    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
        String keyPrefix = timeoutPrefix.split(":" + getLockName(threadId))[0];

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                //获取当前锁的mode的value
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                //如果mode不存在,证明锁已经被释放
                "if (mode == false) then " +
                    //发布释放锁消息
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    //返回true
                    "return 1; " +
                "end; " +
                //如果锁没有释放,并且当前获取锁的并不是本线程,返回null
                "local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
                "if (lockExists == 0) then " +
                    "return nil;" +
                "end; " +

                //如果锁还没有释放,并且是本线程获取的锁,先把对应的计数器(本线程在锁的HASH对应的KEY )减一
                "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
                //如果结果为0了,证明重入次数已经归零,可以完全释放锁了,删掉本线程在锁的HASH对应的KEY
                "if (counter == 0) then " +
                    "redis.call('hdel', KEYS[1], ARGV[2]); " + 
                "end;" +
                //删掉threadId对应的tReadWriteTimeoutName末尾连着获取锁次数的key
                "redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
                //如果HASH中的key数量大于1(就是除了mode还有其他key),证明还有其他线程获取锁了
                "if (redis.call('hlen', KEYS[1]) > 1) then " +
                    //对于pttl命令来说,结果为-2为不存在,结果为-1代表key存在但是没设置过期时间
                    //所以设置maxRemainTime初始为-3,因为要返回最大值
                    "local maxRemainTime = -3; " +
                    //遍历锁的HASH的每一个key
                    "local keys = redis.call('hkeys', KEYS[1]); " +
                    "for n, key in ipairs(keys) do " + 
                        "counter = tonumber(redis.call('hget', KEYS[1], key)); " +
                        //所有的key除了mode以外,其他key的value都是数字(就是获取锁的次数)
                        "if type(counter) == 'number' then " +
                            //锁每获取一次,就会多出一个过期key(对应的tReadWriteTimeout)
                            //利用次数可以拼接出对应的tReadWriteTimeout从而获取到过期时间
                            //从次数到最小为1,拼出对应的tReadWriteTimeout,找出最大的过期时间
                            "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大于零,代表有有效的过期时间,证明锁还没被完全释放,返回false
                    //刷新锁过期时间为maxRemainTime
                    "if maxRemainTime > 0 then " +
                        "redis.call('pexpire', KEYS[1], maxRemainTime); " +
                        "return 0; " +
                    "end;" + 
                    //如果mode为write代表为写锁,为写锁则不算释放,返回false
                    "if mode == 'write' then " + 
                        "return 0;" + 
                    "end; " +
                "end; " +
                //没有其他key,代表锁已经可以被释放,删除这个Lock对应的key,并且发布释放锁的消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; ",
                Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix), 
                LockPubSub.unlockMessage, getLockName(threadId));
    }
}

3.2.2. 写锁源码:

public class RedissonWriteLock extends RedissonLock implements RLock {

    protected RedissonWriteLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }

    @Override
    String getChannelName() {
        return prefixName("redisson_rwlock", getName());
    }

    @Override
    protected String getLockName(long threadId) {
        return super.getLockName(threadId) + ":write";
    }

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

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                            //获取当前mode
                            "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                            //如果mode不存在,证明锁还没被占用,直接获取
                            "if (mode == false) then " +
                                  //设置锁的mode为write
                                  "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                                  //设置当前线程占用锁
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  //设置过期时间
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  //返回null
                                  "return nil; " +
                              "end; " +
                              //如果mode存在且为write
                              "if (mode == 'write') then " +
                                  //当前占用锁的是本线程
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                      //重入,计数加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;" +
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getName()), 
                        internalLockLeaseTime, getLockName(threadId));
    }

    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                //检查mode
                "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                //如果mode不存在则证明锁已释放,发布解锁消息,返回true
                "if (mode == false) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                //如果mode为write
                "if (mode == 'write') then " +
                    //验证获取到锁的是否为当前线程
                    "local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
                    //如果不为,则返回null
                    "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]); " +
                            //返回false
                            "return 0; " +
                        "else " +
                            //如果已经归零,证明锁应该被释放
                            "redis.call('hdel', KEYS[1], ARGV[3]); " +
                            //如果HASH的key数量为1,证明只有mode了,直接释放锁
                            "if (redis.call('hlen', KEYS[1]) == 1) then " +
                                "redis.call('del', KEYS[1]); " +
                                "redis.call('publish', KEYS[2], ARGV[1]); " + 
                            "else " +
                                //不为1的话,肯定是本线程还获取了读锁,将mode修改为read
                                "redis.call('hset', KEYS[1], 'mode', 'read'); " +
                            "end; " +
                            //返回true
                            "return 1; "+
                        "end; " +
                    "end; " +
                "end; "
                //返回null
                + "return nil;",
        Arrays.<Object>asList(getName(), getChannelName()), 
        LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
    }

}

3.3. 场景回顾

回顾一下场景

3.3.1. 场景1

  1. 在时间T1,线程A尝试获取读锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了读锁,之后redis中的数据结构为:

image

  1. 之后在时间T2,线程B尝试获取读锁(过期时间为30s),先调用hget testLock mode检查这个锁的mode,发现mode为read,B也可以获取读锁,获取之后redis中的数据结构为:
    image

  2. 之后在时间T3,线程C尝试获取写锁(过期时间为30s),先调用hget testLock mode检查这个锁的mode,发现mode为read,获取失败

3.3.2. 场景2

  1. 在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
    image

  2. 在时间T2,线程B尝试获取读锁testLock(过期时间为30s)。先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。

  3. 在时间T3,线程C尝试获取写锁testLock(过期时间为30s)。先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的不为自己则获取失败。

3.3.3. 场景3

  1. 在时间T1,线程A尝试获取读锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了读锁,之后redis中的数据结构为:

image

  1. 在时间T2,线程A尝试获取写锁testLock(过期时间为30s)。先调用hget testLock mode检查这个锁的mode,发现mode为read,直接失败

3.3.4. 场景4

  1. 在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
    image

  2. 在时间T2,线程A尝试获取读锁testLock(过期时间为30s)。先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
    image

3.3.4. 场景5

  1. 在时间T1,线程A尝试获取写锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,由于是第一次获取锁,这个并不存在。不存在则直接获取了写锁,之后redis中数据结构为:
    image

  2. 在时间T2,线程A尝试获取写锁testLock(过期时间为30s)。先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
    image

  3. 在时间T3,线程A尝试获取读锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
    image

  4. 在时间T4,线程A尝试获取读锁testLock(过期时间为30s)。首先调用hget testLock mode检查这个锁的mode,发现mode为write,并且获取写锁的就是自己,获取成功,之后redis中数据结构为:
    image

  5. 在时间T5,线程A释放读锁。首先调用hget testLock mode检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次读锁,整体的过期时间戳是剩下的所有key中过期时间戳最大的,就是T3+30s,之后redis中数据结构为:
    image

  6. 在时间T6,线程A释放写锁。首先调用hget testLock mode检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次写锁,
    刷新所整体过期时间为T6+30s,之后redis中数据结构为:
    image

  7. 在时间T7,线程A释放写锁。首先调用hget testLock mode检查这个锁的mode,发现mode为write,证明锁没有释放,释放一次写锁,
    发现写锁的value已经为0,但是还存在其他除mode以外的key,证明本线程还持有读锁,切换成读锁:
    image

  8. 在时间T8,线程B尝试获取读锁(过期时间为30s),先调用hget testLock mode检查这个锁的mode,发现mode为read,B也可以获取读锁,获取之后redis中的数据结构为:
    image

发布了194 篇原创文章 · 获赞 266 · 访问量 145万+

猜你喜欢

转载自blog.csdn.net/zhxdick/article/details/82693646