Redisson 源码解析

3,257 阅读20分钟

一、简单使用

1.1 引入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.5</version>
</dependency>

1.2 基本代码

public static void main(String[] args) throws InterruptedException {
        // 1. Create config object
        Config config = new Config();
        config.useClusterServers() // 使用集群模式
                .setScanInterval(2000) // 集群扫描状态,单位是毫秒
                .addNodeAddress("redis://127.0.0.1:7181");

        // 2. Create Redisson instance
        RedissonClient redisson = Redisson.create(config);


        RLock lock = redisson.getLock("anyLock");
        // 最常见的使用方法
        lock.lock();
        lock.unlock();

        // 加锁以后10秒钟自动解锁
        // 无需调用unlock方法手动解锁
        lock.lock(10, TimeUnit.SECONDS);

        // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
        boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
        if (res) {
            try {
                // do
            } finally {
                lock.unlock();
            }
        }
    }

详细使用,见 wiki

二、源码分析

2.1 可重入锁

                                                if (redis.call('exists', KEYS[1]) == 0) then 
                        redis.call('hincrby', KEYS[1], ARGV[2], 1); 
                        redis.call('pexpire', KEYS[1], ARGV[1]); 
                        return nil; 
                        end; 
                        if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
                        redis.call('hincrby', KEYS[1], ARGV[2], 1); 
                        redis.call('pexpire', KEYS[1], ARGV[1]);
                        return nil; 
                        end; 
                        return redis.call('pttl', KEYS[1]);

这的话,主要就是这段 lua 脚本,分析一下的话,就是:

首先判断 KEYS[1] 这个key 值是否存在,如果存在的话,就设置一个 hash 数据结构,然后设置一个有效期返回。

如果在判断的时候 KEYS[1] 这个 key 已经存在了的话,就会给这个 key 值自增1,然后在重置一下过期时间,最后计算一下还有多长时间过期,然后返回。

这里的 KEYS[1] 就是我们在代码中设置的路径 anyLock ,使用 redisson 的话,每个客户端都会有自己的一个 Manager 类,会有属于自己的一个 UUID ,这里的 ARGV[2] 的值,就是当前的 UUID:threadId ,也就是当前的客户端id,拼接上当前的线程id。

由于 redisson 是有一个 watchdog 这样的一个机制的,默认是 30000ms ,这个实现的功能就是说,如果你这个客户端加了锁,并且在 watchdog 检查的时候,还存在并且客户端还是存活状态,那么watchdog 就会执行续约操作,这样也就是说 ARGV[1] 这个值是 30000ms 。

因为我们这里用的 redisson 的模式是 cluster 模式,因此,在执行 lua 脚本的时候,会首先根据算法,计算出来我们需要在哪个 slot 上,这个 slot 是属于哪个节点的,最终命令会在这个节点上进行执行。

2.2 watchdog

这个机制就是保证如果锁的客户端还存在的话,会一直进行续约操作,每次会续约30秒。如果持有锁的机器宕机了的话,那么机器上的 watchdog 就不会在执行了,不执行以后,锁的时间就会慢慢过期,释放掉这把锁,最多也就是等待30秒。

基本原理的话,就是,客户端在获取到锁之后,会触发一个调度任务,每 10 秒进行一次调用

Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
                
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

主要的核心就是下面这个 lua 脚本

                                                if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
                        redis.call('pexpire', KEYS[1], ARGV[1]); 
                        return 1; 
                        end; 
                        return 0;,

这个逻辑也就是判断这个 key 是否还存在着,存在的话,就直接在次续约 30秒,KEYS[1] 就是我们设置的哪个路径, ARGV[2] 就是当前客户端的 UUID 和 线程ID 的拼接。默认过期时间设置为 30秒。

当这个客户端释放锁之后,这里的key 也就不存在了,那么这个 task 任务也会停止,若客户端宕机的话,也一样会停止,这样的话,这个 key 的过期时间就不会在进行续约,其余客户端最多等待 30秒之后,便可以尝试获取这个锁。

2.3 同步堵塞

                        if (redis.call('exists', KEYS[1]) == 0) then 
                        redis.call('hincrby', KEYS[1], ARGV[2], 1); 
                        redis.call('pexpire', KEYS[1], ARGV[1]); 
                        return nil; 
                        end; 
                        if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
                        redis.call('hincrby', KEYS[1], ARGV[2], 1); 
                        redis.call('pexpire', KEYS[1], ARGV[1]);
                        return nil; 
                        end; 
                        return redis.call('pttl', KEYS[1]);

如果当前已经有一个客户端获取到锁了之后,这时有另外一个客户端来获取锁,key 的话就会是当前客户端的 id 加上当前线程的id , 这样的话,两个 if 都是不满足的,最后就是会返回 当前主 key 的剩余时间。

如果获取锁成功的话, 最后的返回结果是个 nil , 当返回其他的时候,就代表加锁失败,这里返回的是剩余时间,自然不是 nil , 就会走下面这个 while(true) 的死循环,这里主要就是去重试获取锁,如果再次尝试还是获取不到的话,就会等待 ttl 时间后再去获取锁,一直阻塞到获取到锁。

while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }

2.4 释放锁

宕机自动释放锁

如果这个机器宕机了,那么 watchdog 的定时调度任务就没有了,也就是说,最多过 30秒之后,key 就到期了,就会自动释放锁。

主动释放锁
                        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                        "return nil;" +
                        "end; " +
                        "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('del', KEYS[1]); " +
                        "redis.call('publish', KEYS[2], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return nil;",
      Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

这一段 lua 脚本就是执行释放锁的关键所在,

      • KEYS[1] = 我们最开始设置的 key 值,
      • KEYS[2] = getChannelName() = redisson_lock__channel_KEYS[1]
      • ARGV[1] = LockPubSub.UNLOCK_MESSAGE = 0L
      • ARGV[2] = internalLockLeaseTime = 30S
      • ARGV[3] = getLockName(threadId) = 当前锁客户端的 UUID 拼接 当前的线程 ID

这里知道这几个值都代表是什么之后,就比较清晰了:首先判断是不是存在这个锁,如果不存在的话,直接返回 null;

然后,如果这个 key 存在的话,就会对这个锁的值,进行递减,拿到递减之后的结果,对这个结果进行判断,如果是等于 0 , 也就代表就只加锁的了一次,这样就直接通过 del 指令,删除掉这个 key ,并通过发布订阅,发布一条消息。

如果不等于 0 的话,就代表这个可重入锁,加了多次,那么会进行一次过期时间的刷新,在刷新成 30 S。

2.5 尝试获取锁超时与超时锁自动释放

redisson 提供了一个高阶用法,这个就是说,尝试100s,如果100s还获取不到锁就放弃;如果获取到锁,只会占有这个锁 10s , 如果 10s 还没有释放锁,就会自动进行释放。

boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
long time = unit.toMillis(waitTime);
        // 获取到当前时间
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 去尝试获取锁
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return true;
        }
        
        // 对剩余时间进行递减
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        
        current = System.currentTimeMillis();
         ... ... ...

        try {
            // 递减剩余时间 
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        
            while (true) { // 死循环尝试获取,获取失败,递减剩余时间
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // waiting for message
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            unsubscribe(subscribeFuture, threadId);
        }
尝试获取锁超时:

主要就是通过获取到 最开始的时间 ,先进行一次 尝试获取锁 ,若获取锁失败,用 设置的时间 - 用了的时间 ,进入一个 while(true) 的死循环 ,再次尝试获取锁,其逻辑和上面获取锁的逻辑是一样的,如果获取失败,就更新剩余时间,并进行阻塞,等到剩余时间归零之后,会返回 false ,表示获取锁失败。

超时自动释放:

这个逻辑是和 尝试获取锁逻辑有关联的,在尝试获取锁的时候,有这一段代码

if (leaseTime != -1) {
      return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);

如果我们使用的不是这个高阶用法的话,这时 leaseTime 字段值是 -1 , 那么 if 里面就不会走,转而走下面这个, leaseTime 也就变成了默认值 30000ms ,但在这里,我们设置了锁的过期时间是 10s,也就是 lease = 10000ms , 那么在执行加锁的时候,就会设置这个锁的 key ,并将其过期时间设置为 10s。

另外还有一点就是,如果我们设置了 leaseTime ,在这个 if 里面,就直接返回了,如果走默认的话,会根据加锁的返回值,判断需不需要加调度任务,也就是 watchdog , 也就是说,这里我们设置了 leaseTime 之后,是不会加入 watchdog 的,它只有 10s 的生存时间,到时间后,会自动释放。

2.6 隐患

假设客户端刚刚在 master 上写入了一个锁,此时发生了 master 的宕机,master 还每来得及将锁异步同步到 slave ,slave 就切换成了 master , 此时别的客户端在进行加锁的话,会成功获取锁,这时候,就会产生两个客户端持有同一把分布式锁的问题,可能会导致一些数据问题。

2.7 可重入锁总结

  • 加锁:redis 中设置 hash 结构,默认过期时间为 30000ms。
  • 维持加锁:后台有一个调度任务,每10秒钟调度一次,只要客户端和key 都还存在,就会刷新当前 key 的过期时间。
  • 锁互斥:别的客户端或者别的线程再来加锁,会陷入 while(true) 的死循环中,等待。
  • 可重入锁:同一个线程可以加锁多次,每次的话,就是在 hash 结构上自增1。
  • 手动释放锁:在 hash 结构上递减1,对比剩余个数是否为0,为0则直接删除 key。
  • 宕机释放锁:当客户端宕机之后,后台的调度任务就会取消,key 的过期时间就不会在被刷新,默认30s后,key 自动消失。
  • 尝试加锁超时:在循环中,一直尝试获取锁,若时间到了之后,还没有获取到,就退出循环,返回 false。
  • 自动释放锁:在加锁的时候,设置超时时间,这样就不会有调度任务,key 会在设置的过期时间之后过期。

redisson - 可重入锁.png

三、公平锁

3.1 加锁

        RLock fairLock = redisson.getFairLock("anyLock");
        // 最常见的使用方法
        fairLock.lock();

主要就是下面这段 lua 脚本,实现了基本的逻辑,用了 set 和 list 的数据结构

                    "while true do "
                    + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
                    + "if firstThreadId2 == false then "
                        + "break;"
                    + "end; "
                    + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
                    + "if timeout <= tonumber(ARGV[4]) then "
                        + "redis.call('zrem', KEYS[3], firstThreadId2); "
                        + "redis.call('lpop', KEYS[2]); "
                    + "else "
                        + "break;"
                    + "end; "
                  + "end;"
                    
                      + "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "
                            + "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
                            "redis.call('lpop', KEYS[2]); " +
                            "redis.call('zrem', KEYS[3], ARGV[2]); " +
                            "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return nil; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                            "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return nil; " +
                        "end; " +
                            
                        "local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
                        "local ttl; " + 
                        "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + 
                            "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + 
                        "else "
                          + "ttl = redis.call('pttl', KEYS[1]);" + 
                        "end; " + 
                            
                        "local timeout = ttl + tonumber(ARGV[3]);" + 
                        "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
                            "redis.call('rpush', KEYS[2], ARGV[2]);" +
                        "end; " +
                        "return ttl;", 
                        Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName), 
                                    internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime);
KEYS[1]getName()设置的锁key (anyLock)
KEYS[2]threadsQueueNameredisson_lock_queue:{anyLock}
KEYS[3]timeoutSetNameredisson_lock_timeout:{anyLock}
ARGV[1]internalLockLeaseTime30000ms
ARGV[2]getLockName(threadId)客户端的uuid 拼接当前线程 id
ARGV[3]currentTime + threadWaitTime当前时间 + 5000
ARGV[4]currentTime当前时间
3.1.1 首次加锁

我们假设现在只有 客户端A 来进行加锁,首先会通过 redis.call('lindex', redisson_lock_queue:{anyLock}, 0) 这个命令,弹出来这个队列中第一个元素,如果这个队列中没有元素的话,则返回 false ,那么此时会进行判断,如果返回的是 false ,就直接退出 while(true) 的循环。

if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) 
    or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then 

接着会走入上面的命令,此时因为是第一次加锁,所以第一个 exists 肯定是等于 0 ,第二个 exists 也是等于0的 ,满足条件,执行下面的逻辑,lpop 弹出 redisson_lock_queue:{anyLock} ,因为此时队列中是空的,这个也没什么效果,zrem 删除 redisson_lock_timeout:{anyLock} ,这个目前也是空的,后面两个就是之前重入锁的逻辑,去进行 set 赋值,然后设置默认的过期时间为 30s,最后返回一个 nil ,通过上面分析 可重入锁 ,我们知道返回 nil 的时候,就是代表加锁成功的,这时会有一个调度任务 watchdog 每10秒中检查一下 key ,进行key的续约。

                                                        redis.call('lpop', KEYS[2]);
                            redis.call('zrem', KEYS[3], ARGV[2]);
                            redis.call('hset', KEYS[1], ARGV[2], 1); 
                            redis.call('pexpire', KEYS[1], ARGV[1]); 
                            return nil; 
3.1.2 客户端B 加锁

分析完了直接加锁,那么现在让客户端B 也来加锁,首先还是判断 redis.call('lindex', redisson_lock_queue:{anyLock}, 0) , 这时这个队列中还是空的,返回值肯定还是 false ,直接退出 while 循环,继续往下走

                                                local timeout = ttl + tonumber(ARGV[3]);
                        if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then 
                            redis.call('rpush', KEYS[2], ARGV[2]);
                        end;  
                        return ttl;

发现只满足这段逻辑,计算一个 timeout = 当前时间 + 5000ms , 然后执行 zadd redisson_lock_timeout:{anyLock} , timeout, uuid+threadId,这个主要逻辑就是往 set 集合中插入一条数据,分数为 timeout ,值是当前客户端的UUID拼接上当前的线程ID,然后返回过期时间,通过之前分析可重入锁的时候,我们可以知道,当返回值不是 nil 的时候,是代表加锁失败的,这时候就是会进入到 while(true) 的死循环中,间隔时间去尝试重新获取锁。

3.1.3 客户端C 加锁

现在客户端B加锁失败,相关信息也放入到了队列当中,那么 客户端C 此时也来获取锁

local firstThreadId = redis.call('lindex', KEYS[2], 0); 
local ttl; 
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then 
ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);
else 
ttl = redis.call('pttl', KEYS[1]);
end; 

主要就是执行这段逻辑,首先从队列中弹出来第一个元素,元素不为空且不是当前客户端,这时拿到的是 客户端B的值,uuidB+threadIdB,从 set 排序集中获取分数,用获取到的客户端B的分数,减去当前客户端C加锁时的(当前时间 + 5000ms) ,然后在执行客户端 B 加锁的那段,将自己加入到队列和集合中去,进行排队。

                                                local timeout = ttl + tonumber(ARGV[3]);
                        if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then 
                            redis.call('rpush', KEYS[2], ARGV[2]);
                        end;  
                        return ttl;

3.2 可重入加锁

核心逻辑就是获取到哪个 key ,去执行 incr 对值进行递增,重置一下生存时间。

                                                    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
                            redis.call('hincrby', KEYS[1], ARGV[2], 1);
                            redis.call('pexpire', KEYS[1], ARGV[1]); 
                            return nil; 

3.3 队列分数刷新

客户端B 和 客户端C 在循环中一直去尝试获取锁,在执行上面 lua 脚本时,基本都是不满足的 ,会执行到下面这段

"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +
local ttl; " + 
                        "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + 
                            "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + 
                        "else "
                          + "ttl = redis.call('pttl', KEYS[1]);" + 
                        "end; " + 
                            
                        "local timeout = ttl + tonumber(ARGV[3]);" + 
                        "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
                            "redis.call('rpush', KEYS[2], ARGV[2]);" +
                        "end; " +

这时我们假设是客户端B 先执行到这,它会去执行 pttl 看下客户端A上的锁,还有多少的生存时间,然后拿着这个 ttl 加上(当前时间 + 5000) 算出来这个 timeout 的值,再通过 zadd 将客户端B的分数值进行下刷新,这里的话,是客户端B第二次执行 zadd 了,所以这个的返回值是 0 , 就不会再将客户端B 放入到队列中去。

此时,轮到客户端C执行的时候,也是一个同样的逻辑,所以他们在排队集合中的顺序是不会发生改变的且不会多次将自身添加的排队队列中去。

3.4 队列重排

如果客户端B因为各种原因,长时间没有重新去获取锁,导致了分数一直没有刷新,然后客户端C尝试获取锁,从队列中获取到客户端B ,但发现由于客户端B长时间没有更新时间,导致分数值小于了当前时间,那么就会执行 zrem ,和 lpop , 从排序集合和排队队列上去除掉客户端B,现在是在一个 while 循环中,执行完删除之后,客户端C 继续走这段逻辑,从排队队列中获取到自己,发现自己的分数也是小于当前时间的,会继续执行 zream ,rpop ,移除掉自身,走到最后,将自己重新加入到队列和集合中。

这时,客户端B,恢复正常,回来尝试加锁,在走之前的逻辑,让自己重新入队。那么现在排队队列和排序集合中的顺序较之最开始已经发现了变化,这就是队列重排。

"while true do "
                    + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
                    + "if firstThreadId2 == false then "
                        + "break;"
                    + "end; "
                    + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
                    + "if timeout <= tonumber(ARGV[4]) then "
                        + "redis.call('zrem', KEYS[3], firstThreadId2); "
                        + "redis.call('lpop', KEYS[2]); "
                    + "else "
                        + "break;"
                    + "end; "
                  + "end;"

从这里,我们可以看到,在一个客户端刚刚加锁之后,其他的客户端来争抢着把锁,刚开始在一定时间范围之内,时间不要过长,各个客户端是可以按照公平的节奏,在队列和集合中保持有序的。

在一定时间范围内,时间不要过长,这样队列和集合中的数据顺序是不会变得,各个客户端都会定期刷新自己的分数值。

但如果客户端A 持有锁的时间过长 , 可能会在 while true 的死循环中将一些等待时间过长的客户端从队列和集合中删除,一旦删除之后,就会发生各个客户端随着自己重新尝试加锁的时间顺序,重新进行排序,加入到队列和集合中。

3.5 释放锁

客户端A 来释放锁的时候,也是会过来走这个 while true 的循环,看一下有序集合中的元素的 timeout 时间,如果小于了当前时间,就将其删除掉,让他后面的都进行一下重排序。

在这的话,客户端B 和 客户端C 在尝试获取锁的时候,都是用的 tryAcquire 方法,会有一个获取锁的超时时间,当超过这个时间之后,就不会再去尝试获取锁了,但队列和集合中的数据还是存在的,所以这个 while true 就是会对这种数据进行剔除。哪怕客户端宕机了,那么分数就不会刷新,在执行 while true 的时候,早晚会将其从集合和队列中移除。

后面的逻辑就是进行一些判断,发布一些消息,如果一切正常的话,就是会执行 del 命令,将 key 删除掉。因为是可重入,会进行递减1,判断是否为 0 ,不为0则重置一下生存时间。

"while true do "
                + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
                + "if firstThreadId2 == false then "
                    + "break;"
                + "end; "
                + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
                + "if timeout <= tonumber(ARGV[4]) then "
                    + "redis.call('zrem', KEYS[3], firstThreadId2); "
                    + "redis.call('lpop', KEYS[2]); "
                + "else "
                    + "break;"
                + "end; "
              + "end;"
                
              + "if (redis.call('exists', KEYS[1]) == 0) then " + 
                    "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
                    "if nextThreadId ~= false then " +
                        "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
                    "end; " +
                    "return 1; " +
                "end;" +
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "end; " +
                    
                "redis.call('del', KEYS[1]); " +
                "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
                "if nextThreadId ~= false then " +
                    "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
                "end; " +
                "return 1; ",

当客户端A 将锁释放后,这时,客户端C再去走加锁逻辑,这时 key 是空的了,而且队头是就是客户端C,那么这时,客户端C 就会将 自己从队列和集合中移除,然后在设置锁key,设置超时时间,返回 nil ,增加 watchdog。

四、MultiLock

将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁,一次性锁定多个资源,再去处理任务,然后时候一次性释放

4.1 代码示例

public static void main(String[] args) {
    RLock lock1 = redissonInstance1.getLock("lock1");
    RLock lock2 = redissonInstance2.getLock("lock2");
    RLock lock3 = redissonInstance3.getLock("lock3");

    RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
    // 同时加锁:lock1 lock2 lock3
    // 所有的锁都上锁成功才算成功。
    lock.lock();
    ...
    lock.unlock();
}

4.2 加锁

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long baseWaitTime = locks.size() * 1500;
        long waitTime = -1;
        if (leaseTime == -1) {
            waitTime = baseWaitTime;
            unit = TimeUnit.MILLISECONDS;
        } else {
            waitTime = unit.toMillis(leaseTime);
            if (waitTime <= 2000) {
                waitTime = 2000;
            } else if (waitTime <= baseWaitTime) {
                waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime);
            } else {
                waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime);
            }
            waitTime = unit.convert(waitTime, TimeUnit.MILLISECONDS);
        }
        
        while (true) {
            if (tryLock(waitTime, leaseTime, unit)) {
                return;
            }
        }
    }

这里的加锁逻辑比较简单,首先就是会根据锁的数量计算出来一个 baseWaitTime 这里这个值等于 4500,然后进入 while(true) 的死循环,通过 tryLock() 方法去获取锁,不过要注意的是,这里使用的是 tryLock() ,制定了获取锁的最大等待时间为 2000,默认过期时间为 30000毫秒。

** **

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        
        long newLeaseTime = -1;
        if (leaseTime != -1) {
            newLeaseTime = unit.toMillis(waitTime)*2;
        }
        
        long time = System.currentTimeMillis();
        long remainTime = -1;
        if (waitTime != -1) {
            remainTime = unit.toMillis(waitTime);
        }
        long lockWaitTime = calcLockWaitTime(remainTime);
        
        int failedLocksLimit = failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
        for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
           
               long awaitTime = Math.min(lockWaitTime, remainTime);
               lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            
            
            if (lockAcquired) {
                acquiredLocks.add(lock);
            } else {
               ... ...
            }
            // 计算时间
            if (remainTime != -1) {
                remainTime -= (System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                if (remainTime <= 0) { // 超时之后,释放掉锁
                    unlockInner(acquiredLocks);
                    return false;
                }
            }
        }

        ... ...
        
        return true;
    }

简单来说的话,这里循环遍历我们设置好的那个 lock 集合,遍历去尝试获取锁,每次获取完锁之后,计算下剩余的时候,如果在 4500毫秒之内没有全都获取到锁,会将之前已经获取到的锁,进行释放。然后返回false,在while(true) 的死循环中,继续执行。

4.3 释放锁

public void unlock() {
        List<RFuture<Void>> futures = new ArrayList<RFuture<Void>>(locks.size());

        for (RLock lock : locks) {
            futures.add(lock.unlockAsync());
        }

        for (RFuture<Void> future : futures) {
            future.syncUninterruptibly();
        }
    }

释放锁的逻辑很简单,就是遍历这个 lock 集合,调用 lua 脚本释放锁,然后有一个等待全部执行完毕的 future。

五、RedLock

5.1 原理

如果我们要在 cluster 模式下,获取一把分布式锁,需要经过如下步骤:

  1. 获取到要执行的时候的当前时间戳;
  2. 尝试在每个 master 上创建锁,设置一个较短的过期时间,一般几十毫秒的样子,在创建锁的过程中,设置一个超时时间,如果过了这个超时时间还没有获取成功,就按失败算;
  3. 尝试在大多数节点上创建出来锁 (n / 2 + 1);
  4. 客户端计算建好锁的时间,如果建立锁的时间小于超时时间,就算创建成功;
  5. 如果锁创建失败了,就删除掉已经创建的锁;
  6. 只要别人创建了一把分布式锁,就得不断轮训去尝试偶去锁

普通的 redis 分布式锁,其实是通过 hash 算法,选择一台实例创建锁就可以,但是 RedLock , 需要在 n / 2 + 1 个节点上创建成功,才算是整体成功,避免说仅仅在一个节点实例上加锁。

5.2 算法实现

public static void main(String[] args) {
         RLock lock1 = redissonInstance1.getLock("lock1");
        RLock lock2 = redissonInstance2.getLock("lock2");
        RLock lock3 = redissonInstance3.getLock("lock3");

        RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
        // 同时加锁:lock1 lock2 lock3
        // 红锁在大部分节点上加锁成功就算成功。
        lock.lock();
        ...
        lock.unlock();   
}
public class RedissonRedLock extends RedissonMultiLock {

    /**
     * Creates instance with multiple {@link RLock} objects.
     * Each RLock object could be created by own Redisson instance.
     *
     * @param locks - array of locks
     */
    public RedissonRedLock(RLock... locks) {
        super(locks);
    }

    @Override
    protected int failedLocksLimit() {
        return locks.size() - minLocksAmount(locks);
    }
    
    protected int minLocksAmount(final List<RLock> locks) {
        return locks.size()/2 + 1;
    }

    @Override
    protected long calcLockWaitTime(long remainTime) {
        return Math.max(remainTime / locks.size(), 1);
    }
    
    @Override
    public void unlock() {
        unlockInner(locks);
    }

}

其实,redLock 这个类是 MultiLock 的一个子类,它重写了相关参数的一个计算逻辑,这里我们打三个 lock 计算

    • failedLocksLimit :lock.size - lock.size() / 2 + 1 = 1 , 这个代表允许失败的个数。
    • calcLockWaitTime : remainTime / lock.size() = 4500 / 3 = 1500 , 获取每个小锁的最大等待时间
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
            RLock lock = iterator.next();
            boolean lockAcquired;

                    lockAcquired = lock.tryLock();
                ... ...
            
            if (lockAcquired) {
                acquiredLocks.add(lock);
            } else {
                if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                    break;
                }

                if (failedLocksLimit == 0) {
                    unlockInner(acquiredLocks);
                    if (waitTime == -1 && leaseTime == -1) {
                        return false;
                    }
                    failedLocksLimit = failedLocksLimit();
                    acquiredLocks.clear();
                    // reset iterator
                    while (iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    failedLocksLimit--;
                }
            }
            
            if (remainTime != -1) {
                remainTime -= (System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                if (remainTime <= 0) {
                    unlockInner(acquiredLocks);
                    return false;
                }
            }
        }
}

这里获取锁的逻辑没有什么变化,主要就是当尝试获取锁失败之后,会进行 else 中, lock.size - 已经获取到锁的集合的长度 == 允许失败的个数 ,如果满足这个条件就直接 break ,跳出这个 for 循环,并返回 true ,表示加锁成功;如果不满足的话,会执行 允许失败的个数-- ,直到个数为0,释放掉已经加过的锁,并返回 false ,表示整体加锁失败。

这里设置了三个 key ,这三个锁key 是会分布在不同的三个 redis master 实例上的,此时,别人过来加锁,用的是一样的 key ,是无法加锁成功的,因为锁已经被占用了,就会进入一个 while(true) 循环,尝试获取锁,逻辑就和之前是一样的。

六、读写锁

6.1 读锁

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

加锁主要就是在执行这段脚本,首先我们对参数进行下分析:

    • KEYS[1] : 当前设置的锁Key = anyLock
    • KEYS[2] : {锁key}:客户端的uuid:threadId:rwlock_timeout = {anyLock}:UUID_01:ThreadId_01:rwlock_timeout
    • ARGV[1] : 默认过期时间 = 30000ms
    • ARGV[2] : 客户端UUID:threadID = UUID_01:ThreadId_01
    • ARGV[3] : 客户端UUID:threadID:write = UUID_01:ThreadId_01:write

首先就行从 anyLock 中获取key为 mode 的值,如果值不存在的话,返回的就是 false ,会执行相关语句:

hset anyLock mode read , 设置一个hash 值

hset anyLock UUID_01:ThreadId_01 1 , 设置 key 值 为 1

set {anyLock}:UUID_01:ThreadId_01:rwlock_timeout:1 1 设置 key 值 为 1

pexpire {anyLock}:UUID_01:ThreadId_01:rwlock_timeout 30000 设置30s的过期时间

pexpire anyLock 30000 设置 30s 的过期时间

然后返回 nil ,表示加锁成功,之后的逻辑和之前一样,如果加锁成功的话,会触发一个调度任务,增加 watchdog , 每隔10秒,检查一下 key 是否还被占有,刷新一下生存时间。

"local counter = redis.call('hget', KEYS[1], ARGV[2]); " +
                "if (counter ~= false) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    
                    "if (redis.call('hlen', KEYS[1]) > 1) then " +
                        "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 " + 
                                    "redis.call('pexpire', KEYS[2] .. ':' .. key .. ':rwlock_timeout:' .. i, ARGV[1]); " + 
                                "end; " + 
                            "end; " + 
                        "end; " +
                    "end; " +
                    
                    "return 1; " +
                "end; " +
                "return 0;",
            Arrays.<Object>asList(getName(), keyPrefix), 
            internalLockLeaseTime, getLockName(threadId));

还是一样,先来分析一下各个参数都是什么意思:

    • KEYS[1] : 锁key = anyLock
    • KEYS[2] : {anyLock}
    • ARGV[1] : 默认过期时间 30000ms
    • ARGV[2] : 客户端UUID:ThreadId = UUID_01:ThreadId_01

首先就是去获取加锁时的那个 hash 结构中,加锁客户端的值,判断如果有值的话,就直接将 anyLock 值刷新为默认的 30秒。

然后再去判断 anyLock 这个 hash 结构中是否有多个 key ,这里是满足的,获取到全部的 key ,遍历,找到值是 number 类型的key ,这里只有上面设置的 **hset anyLock UUID_01:ThreadId_01 1 是满足的,**因为读锁是可以加多个的,也可以说是可重入的,所以这里就是获取到加锁的个数,然后通过遍历,每次递减一,调用 pexpire {anyLock}:anyLock UUID_01:ThreadId_01:rwlock_timeout:1 30000ms,刷新生存时间,这个值其实就是上面加锁时设置的 set {anyLock}:UUID_01:ThreadId_01:rwlock_timeout:1 1 ,最后返回 1,表示刷新成功。

6.2 写锁

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

KEYS[1] = anyLock ,

ARGV[1] = 30000ms , ARGV[2] = UUID_01:ThreadId_01:write

首先还是从 mode 中获取值,默认是没有的,就会执行下面的执行,set anyLock mode write,

set anyLock UUID_01:ThreadId_01:write 1 , 然后设置默认的过期时间,这里的主要逻辑基本和读锁是一致的,就是欢乐一下 mode 为 write。

6.3 读锁读锁非互斥

"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]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);",
  • KEYS[1] : 当前设置的锁Key = anyLock
  • KEYS[2] : {锁key}:客户端的uuid:threadId:rwlock_timeout = {anyLock}:UUID_02:ThreadId_02:rwlock_timeout
  • ARGV[1] : 默认过期时间 = 30000ms
  • ARGV[2] : 客户端UUID:threadID = UUID_02:ThreadId_02
  • ARGV[3] : 客户端UUID:threadID:write = UUID_02:ThreadId_02:write

这里的话,首先客户端A是已经获取到读锁的,这时候客户端B要来加读锁,这里会先判断 mode 是不是 read ,或者说,加的是写锁,但加写锁的是当前的客户端,这样就可以走下面的逻辑,将 UUID_02:ThreadId_02 递增1,获取到的 ind 的值也是1,然后拼接 key {anyLock}:UUID_02:ThreadId_02:rwlock_timeout:1 , 设置这个key值为1,并设置过期时间,在刷新主 key 的值。

当加锁成功之后,会设置调度任务,也就是 watchdog ,进行执行刷新生存周期的逻辑,这个就和之前分析的是一样。

执行到这里我们可以发现,读锁和读锁是不互斥的,只会在 anyLock 的主key中新增 key值,并设置当前客户端的值。

6.4 读锁写锁互斥

其实走上面的 lua 脚本就可以发现,如果加了读锁在去加写锁的话,是不会走任何一个 if 的,最后之后执行 ttl 指令,返回一个剩余时间,这时就代表是加锁失败,客户端会进入一个 while(true) 的死循环,尝试获取锁。

当先加了写锁再去加读锁的时候,只有是当前客户端加的写锁,才能进入 if 中,设置相对应的 key 信息,反之,加锁失败。