Redisson 实现 Redis 分布式锁的N种姿势

2019/2/27 posted in  Cache

原文地址 https://mp.weixin.qq.com/s/EcwPnD8jlZrBUzcADcRl6A

来源:阿飞的博客

前几天发的一篇文章《Redlock:Redis 分布式锁最牛逼的实现》,引起了一些同学的讨论,也有一些同学提出了一些疑问,这是好事儿。本文在讲解如何使用 Redisson 实现 Redis 普通分布式锁,以及 Redlock 算法分布式锁的几种方式的同时,也附带解答这些同学的一些疑问。

Redis 几种架构

Redis 发展到现在,几种常见的部署架构有:

  1. 单机模式;
  2. 主从模式;
  3. 哨兵模式;
  4. 集群模式;

我们首先基于这些架构讲解 Redisson 普通分布式锁实现,需要注意的是,只有充分了解普通分布式锁是如何实现的,才能更好的了解 Redlock 分布式锁的实现,因为 Redlock 分布式锁的实现完全基于普通分布式锁

普通分布式锁

Redis 普通分布式锁原理这个大家基本上都了解,本文不打算再过多的介绍,上一篇文章Redlock:Redis 分布式锁的实现也讲的很细,并且也说到了几个重要的注意点。如果你对 Redis 普通的分布式锁还有一些疑问,可以再回顾一下这篇文章。

接下来直接 show you the code,毕竟 talk is cheap。

  • redisson 版本

本次测试选择 redisson 2.14.1 版本。

单机模式

源码如下:

// 构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://172.29.1.180:5379").setPassword("a123456").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("DISLOCK");
boolean isLock;
try {
    //尝试获取分布式锁
    isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(15000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    disLock.unlock();
}

通过代码可知,经过 Redisson 的封装,实现 Redis 分布式锁非常方便,我们再看一下 Redis 中的 value 是啥,和前文分析一样,hash 结构,key 就是资源名称,field 就是 UUID+threadId,value 就是重入值,在分布式锁时,这个值为 1(Redisson 还可以实现重入锁,那么这个值就取决于重入次数了):

172.29.1.180:5379> hgetall DISLOCK
1) "01a6d806-d282-4715-9bec-f51b9aa98110:1"
2) "1"

哨兵模式

即 sentinel 模式,实现代码和单机模式几乎一样,唯一的不同就是 Config 的构造:

Config config = new Config();
config.useSentinelServers().addSentinelAddress(
        "redis://172.29.3.245:26378","redis://172.29.3.245:26379", "redis://172.29.3.245:26380")
        .setMasterName("mymaster")
        .setPassword("a123456").setDatabase(0);

集群模式

集群模式构造 Config 如下:

Config config = new Config();
config.useClusterServers().addNodeAddress(
        "redis://172.29.3.245:6375","redis://172.29.3.245:6376", "redis://172.29.3.245:6377",
        "redis://172.29.3.245:6378","redis://172.29.3.245:6379", "redis://172.29.3.245:6380")
        .setPassword("a123456").setScanInterval(5000);

总结

普通分布式实现非常简单,无论是那种架构,向 Redis 通过 EVAL 命令执行 LUA 脚本即可。

Redlock 分布式锁

那么 Redlock 分布式锁如何实现呢?以单机模式 Redis 架构为例,直接看实现代码:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://172.29.1.180:5378")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://172.29.1.180:5379")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://172.29.1.180:5380")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(30000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    System.out.println("");
    redLock.unlock();
}

最核心的变化就是RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);,因为我这里是以三个节点为例。

那么如果是哨兵模式呢?需要搭建 3 个,或者 5 个 sentinel 模式集群(具体多少个,取决于你)。
那么如果是集群模式呢?需要搭建 3 个,或者 5 个 cluster 模式集群(具体多少个,取决于你)。

实现原理

既然核心变化是使用了 RedissonRedLock,那么我们看一下它的源码有什么不同。这个类是 RedissonMultiLock 的子类,所以调用 tryLock 方法时,事实上调用了 RedissonMultiLock 的 tryLock 方法,精简源码如下:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 实现要点之允许加锁失败节点限制(N-(N/2+1))
    int failedLocksLimit = failedLocksLimit();
    List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
    // 实现要点之遍历所有节点通过EVAL命令执行lua加锁
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        try {
            // 对节点尝试加锁
            lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
        } catch (RedisConnectionClosedException|RedisResponseTimeoutException e) {
            // 如果抛出这类异常,为了防止加锁成功,但是响应失败,需要解锁
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            // 抛出异常表示获取锁失败
            lockAcquired = false;
        }

        if (lockAcquired) {
            // 成功获取锁集合
            acquiredLocks.add(lock);
        } else {
            // 如果达到了允许加锁失败节点限制,那么break,即此次Redlock加锁失败
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }               
        }
    }
    return true;
}

很明显,这段源码就是上一篇文章Redlock:Redis 分布式锁的实现提到的 Redlock 算法的完全实现。

以 sentinel 模式架构为例,如下图所示,有 sentinel-1,sentinel-2,sentinel-3 总计 3 个 sentinel 模式集群,如果要获取分布式锁,那么需要向这 3 个 sentinel 集群通过 EVAL 命令执行 LUA 脚本,需要 3/2+1=2,即至少 2 个 sentinel 集群响应成功,才算成功的以 Redlock 算法获取到分布式锁:

Redlock 分布式锁

问题合集

image.png

根据上面实现原理的分析,这位同学应该是对 Redlock 算法实现有一点点误解,假设我们用 5 个节点实现 Redlock 算法的分布式锁。那么要么是 5 个 redis 单实例,要么是 5 个 sentinel 集群,要么是 5 个 cluster 集群。而不是一个有 5 个主节点的 cluster 集群,然后向每个节点通过 EVAL 命令执行 LUA 脚本尝试获取分布式锁,如上图所示。

  • 失效时间如何设置

这个问题的场景是,假设设置失效时间 10 秒,如果由于某些原因导致 10 秒还没执行完任务,这时候锁自动失效,导致其他线程也会拿到分布式锁。

这确实是 Redis 分布式最大的问题,不管是普通分布式锁,还是 Redlock 算法分布式锁,都没有解决这个问题。也有一些文章提出了对失效时间续租,即延长失效时间,很明显这又提升了分布式锁的复杂度。另外就笔者了解,没有现成的框架有实现,如果有哪位知道,可以告诉我,万分感谢。

  • redis 分布式锁的高可用

关于 Redis 分布式锁的安全性问题,在分布式系统专家 Martin Kleppmann 和 Redis 的作者 antirez 之间已经发生过一场争论。有兴趣的同学,搜索 " 基于 Redis 的分布式锁到底安全吗 " 就能得到你想要的答案,需要注意的是,有上下两篇(这应该就是传说中的神仙打架吧,哈)。

  • zookeeper or redis

没有绝对的好坏,只有更适合自己的业务。就性能而言,redis 很明显优于 zookeeper;就分布式锁实现的健壮性而言,zookeeper 很明显优于 redis。如何选择,取决于你的业务!