阅读 1296

Redisson 实现RedLock详解

要实现分布式锁,Redis官网介绍了三个必须要保证的特性:

  • 安全特性:互斥。任意时刻都只能有一个客户端能够持有锁。
  • 活跃性A:无死锁。即使在持有锁的客户端崩溃,或者出现网络分区的情况下,依然能够获取锁。
  • 活跃性B:容错。只要多数Redis节点是存活状态,客户端就能申请锁或释放锁。

为什么基于故障转移的分布式锁还不够呢

我们分析一个场景:

1.客户端A在Redis master节点申请锁

2.master在将存储的key同步到slave上之前崩溃了

3.slave晋升为master

4.客户端B申请一个客户端A已经持有的资源的锁

哇,问题出现了。

Redlock算法

为了解决基于故障转移实现的分布式锁的问题,Redis作者提出了Redlock算法。

假设有N(N≥5)个Redis的master实例,这些节点之间都是相互独立的,因此我们不用副本或其他隐式的协调系统。为了获取锁,客户端需要进行以下操作(假设N=5):

1.获取当前时间

2.按顺序尝试在5个Redis节点上获取锁,使用相同的key 作为键,随机数作为值(随机值在5个节点上是一样的)。在尝试在每个Redis节点上获取锁时,设置一个超时时间,这个超时时间需要比总的锁的自动超时时间小。例如,自动释放时间为10秒,那么连接超时的时间可以设置为5-50毫秒。这样可以防止客户端长时间与处于故障状态的Redis节点通信时保持阻塞状态:如果一个Redis节点处于故障状态,我们需要尽快与下一个节点进行通信。

3.客户端计算获取锁时消耗的时间,用当前时间,减去在第1步中得到的时间。只用当客户端可以在多数节点上能够获取到锁,并且获取锁消耗的总时间小于锁的有效时间,那么这个锁被认为是获取成功了。

4.如果锁获取成功了,锁的有效时间是初始的有效时间减掉申请锁消耗的总时间。

5.如果客户端申请锁失败了(例如不满足多数节点或第4步中获取到的锁有效期为负数),客户端需要在所有节点上解除锁(即使是认为已经无法提供服务的节点)。

当一个客户端无法获取锁时,它应该在随机延迟后再次尝试,以便让多个客户端在同一时间尝试获取相同资源的锁(可能会导致脑裂的情况),而且,在大多数Redis实例中,客户端尝试获取锁定的速度越快,出现裂脑情况的窗口就越小,因此,理想情况下,客户端应尝试使用多路复用将SET命令发送到N个实例。

值得强调的是,对于未能获取大多数锁的客户端,尽快释放(部分)获取的锁,这是多么重要,因此,无需等待key到期即可再次获取锁(但是,如果发生网络分区,并且客户端不再能够与Redis实例通信,则在等待key到期时需要支付可用性损失)。

有细心的同学可能会有疑惑,在每个节点上设置的锁时间是一样的,但是在每个节点执行的时间可能不一样,那锁失效的时间也有早有晚啊,那这个算法够安全吗?我们一起来分析一下不同场景下的情况:

首先假设客户端在每个节点上都能获取到锁,每个节点上都有相同的存活时间的key。但是,key在每个节点上设置的时间点是不一样的,那么key的过期时间就不一样,假设第一个节点上设置锁的时间点为T1,最后一个节点设置锁的时间点为T2,那么第一个锁的有效期至少为

MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT(每个计算机都有一个本地时间,而每个计算机可能都存在非常小的时间误差,这个时间误差即为CLOCK_DRIFT),其他节点上的key都会在这个时间之后才会到期,所以我们可以确定至少在这一时间内,所有的key是都生效的。

在一个客户端已经在多数节点上设置了key的这段时间内,其他的客户端是无法再在多数节点上(N/2+1)获取到锁的。

我们还需要保证多个客户端在同一时间不能获取到同一个锁。

如果一个客户端在多数节点上获取锁所消耗的时间接近或者大于锁的有效期(TTL),我们认为这个锁没有申请成功,并会在所有节点上进行解锁操作,所以我们只要考虑加锁消耗时间小于锁有效期的情况即可,在这种情况下,对于上面已经说明的参数——MIN_VALIDITY,没有客户端能够重新获取锁。所以只有在在多数节点上(N/2+1)加锁耗时大于TTL的情况下,才会有多个客户端可能会同时获取到锁,这种情况直接将锁认定为获取失败即可。

这套算法的可用性是基于以下三个特性:

1.自动释放锁——锁可以自动失效,失效后可以重新上锁

2.通常情况下,客户端通常会在未获得锁或获得锁且工作终止时删除锁,这使得我们不必等待key过期即可重新获得锁。

3.当客户端需要重试获取锁时,它等待的时间要比获取大多数节点锁定所需的时间长得多,以便概率地使资源争用期间的脑裂情况变得不可能。

然而,我们付出了时间为TTL的网络分区可用性的代价,如果发生了连续性的分区,那我们也要付出无限的分区可用性代价(CAP理论,一致性、可用性、分区容错性,分区一致性是前提)。每当客户端获得一个锁并在能够删除锁之前被分区时,都会发生这种情况。(如下图所示,有三个节点的分区不可用的情况下,client A 和client B同时申请一个锁,这时两个客户端都无法获取到多数节点,那么一直到网络恢复或者锁的超时时间,竞争关系解除,重新再竞争...)

Redisson的实现

以上就是Redis的作者给出的Redlock的算法模型,那么在Java的Redis客户端中,Redisson实现了Redlock,我们来分析一下它的具体实现代码。

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

按照惯例,我们先来看一下源码

/**
 * RedLock locking algorithm implementation for multiple locks. 
 * It manages all locks as one.
 * 
 * @see <a href="http://redis.io/topics/distlock">http://redis.io/topics/distlock</a>
 *
 * @author Nikita Koksharov
 *
 */
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);
    }

}
复制代码

通过构造函数我们可以看出,要构造出一个RedissonRedLock,需要至少一个RLock实例,具体实现需要到父类RedissonMultiLock中查看,父类其实是将构造函数传入的RLock添加到了一个List的列表中。至于什么是RLock呢,这是Redisson中对锁的最高层的抽象,它的实现类包括RedissonWriteLock、RedissonReadLock、RedissonFairLock,当然我们正在分析的RedissonMultiLock也是它的实现类。

假设我们有三个Redis节点,

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://localhost:5378")
        .setPassword("").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
 
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://localhost:5379")
        .setPassword("").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
 
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://localhost:5380")
        .setPassword("").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
 
String lockKey = "REDLOCK";
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);


RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
    if (redLock.tryLock(10, 5, TimeUnit.SECONDS)) {
        //TODO if get lock success, do something;
    }
 }catch(Exception e){
    
 }
复制代码

RedissonRedLock完全的按照上文我们介绍的Redlock的算法来实现的,通过在三个不同节点上分别获取锁,来构造一个Redlock,我们再来分析一下具体的tryLock的实现,这个方法是在RedissonRedLock的父类RedissonMultiLock实现的:

 /**
     *
     * @param waitTime the maximum time to acquire the lock
     * @param leaseTime lease time
     * @param unit time unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1;
        if (leaseTime != -1) {
            if (waitTime == -1) {
                newLeaseTime = unit.toMillis(leaseTime);
            } else {
                //等待时间和加锁时间都不为-1时,newLeaseTime为waitTime时间的两倍
                newLeaseTime = unit.toMillis(waitTime)*2;
            }
        }
        
        long time = System.currentTimeMillis();
        long remainTime = -1;
        if (waitTime != -1) {
            remainTime = unit.toMillis(waitTime);
        }
        //子类重写的方法,Math.max(remainTime / locks.size(), 1),waitTime除以节点的个数,与1取较大值
        //Math.max(等待时间的毫秒数/节点个数,1)
        long lockWaitTime = calcLockWaitTime(remainTime);

        //调用子类重写方法,locks.size() - minLocksAmount(locks)
        //minLocksAmount(locks) => locks.size()/2 + 1
        //failedLocksLimit = 锁的个数 -(锁的个数/2 + 1)
        //即为Redis节点个数的少数(N/2-1),获取锁允许失败个数的最大阀值为N/2-1,超过这个值,就认定Redlock加锁失败
        int failedLocksLimit = failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList<>(locks.size());
        for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
            RLock lock = iterator.next();
            boolean lockAcquired;
            try {
                if (waitTime == -1 && leaseTime == -1) {
                    lockAcquired = lock.tryLock();
                } else {
                    //加锁尝试的等待时间为:等待时长的毫秒数 与 Math.max(等待时长的毫秒数/节点个数,1)之间的较小值
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    //挨个尝试加锁,锁的有效期为等待时长的2倍
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException e) {
                //申请锁超时就尝试解锁
                unlockInner(Arrays.asList(lock));
                lockAcquired = false;
            } catch (Exception e) {
                lockAcquired = false;
            }
            
            if (lockAcquired) {
                //加锁成功的节点就放到acquiredLocks这个list中
                acquiredLocks.add(lock);
            } else {
                //加锁失败,需要判断失败的个数是否已经达到了N/2-1个,达到了的话,再来一个失败的,那么这个
                //redlock就加锁失败了,后面的就可以不用再试了
                if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                    break;
                }

                if (failedLocksLimit == 0) {
                    unlockInner(acquiredLocks);
                    if (waitTime == -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;
                }
            }
        }

        //redLock申请成功,为每个节点上的锁设置过期时间
        if (leaseTime != -1) {
            List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
            for (RLock rLock : acquiredLocks) {
                RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                futures.add(future);
            }
            
            for (RFuture<Boolean> rFuture : futures) {
                rFuture.syncUninterruptibly();
            }
        }
        
        return true;
    }
    
复制代码

加锁的代码大家可以通过看注释,根据上文的算法原理解析,应该可以轻松的看懂,接下来再看一下解锁:

 protected RFuture<Void> unlockInnerAsync(Collection<RLock> locks, long threadId) {
        if (locks.isEmpty()) {
            return RedissonPromise.newSucceededFuture(null);
        }
        
        RPromise<Void> result = new RedissonPromise<Void>();
        AtomicInteger counter = new AtomicInteger(locks.size());
        for (RLock lock : locks) {
            lock.unlockAsync(threadId).onComplete((res, e) -> {
                if (e != null) {
                    result.tryFailure(e);
                    return;
                }
                //在所有Redis节点上都完成解锁动作后
                if (counter.decrementAndGet() == 0) {
                    result.trySuccess(null);
                }
            });
        }
        return result;
    }
    
复制代码

Redlock虽然是Redis作者踢出的一种实现分布式锁的算法,但是,它也并不是一个实现分布式锁的完美算法,如果对Redlock的缺点有兴趣,大家可以看一下Martin Kleppmann批判Redlock的文章(叫Martin的好像都挺牛,Martin Fowler) martin.kleppmann.com/2016/02/08/…

不得不佩服大师,系统各种情况、场景都考虑的非常全面,向大师致敬,我们在学习知识的同时,也需要自己多思考,多总结。

文章分类
后端
文章标签