乐趣区

Redis集群环境下的-RedLock(真分布式锁) 实践

在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。有很多三方库和文章描述如何用 Redis 实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增加一点复杂的设计就可以获得更好的可靠性。这篇文章的目的就是尝试提出一种官方权威的用 Redis 实现分布式锁管理器的算法,我们把这个算法称为 RedLock。
Redlock 是 redis 官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。关于这个算法的讨论可以看下官方文档。
https://github.com/antirez/re…
安全和可靠性保证
在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。
1、一致性:互斥,不管任何时候,只有一个客户端能持有同一个锁。2、分区可容忍性:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。3、可用性:只要大多数 Redis 节点正常工作,客户端应该都能获取和释放锁。
为什么基于故障切换的方案不够好
为了理解我们想要提高的到底是什么,我们先看下当前大多数基于 Redis 的分布式锁三方库的现状。用 Redis 来实现分布式锁最简单的方式就是在实例里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是 Redis 自带的超时特性),所以每个锁最终都会释放。
而当一个客户端想要释放锁时,它只需要删除这个键值即可。表面来看,这个方法似乎很管用,但是这里存在一个问题:在我们的系统架构里存在一个单点故障,如果 Redis 的 master 节点宕机了怎么办呢?有人可能会说:加一个 slave 节点!在 master 宕机时用 slave 就行了!但是其实这个方案明显是不可行的,因为这种方案无法保证第 1 个安全互斥属性,因为 Redis 的复制是异步的。总的来说,这个方案里有一个明显的竞争条件(race condition),举例来说:
1、客户端 A 在 master 节点拿到了锁。2、master 节点在把 A 创建的 key 写入 slave 之前宕机了。3、slave 变成了 master 节点 4、B 也得到了和 A 还持有的相同的锁(因为原来的 slave 里还没有 A 持有锁的信息)
当然,在某些特殊场景下,前面提到的这个方案则完全没有问题,比如在宕机期间,多个客户端允许同时都持有锁,如果你可以容忍这个问题的话,那用这个基于复制的方案就完全没有问题,否则的话我们还是建议你采用这篇文章里接下来要描述的方案。
Redlock 简介
在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:
1、安全属性:互斥,不管什么时候,只有一个客户端持有锁 2、效率属性 A: 不会死锁 3、效率属性 B:容错,只要大多数 redis 节点能够正常工作,客户端端都能获取和释放锁。
Redlock 算法
在分布式版本的算法里我们假设我们有 N 个 Redis master 节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把 N 设成 5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行 5 个 master 节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
1、获取当前时间(单位是毫秒)。
2、轮流用相同的 key 和随机值在 N 个节点上请求锁,在这一步里,客户端在每个 master 上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是 10 秒钟,那每个节点锁请求的超时时间可能是 5 -50 毫秒的范围,这个可以防止一个客户端在某个宕掉的 master 节点上阻塞过长时间,如果一个 master 节点不可用了,我们应该尽快尝试下一个 master 节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数 master 节点上成功获取了锁(在这里是 3 个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1) 还是因为总消耗时间超过了锁释放时间,客户端都会到每个 master 节点上释放锁,即便是那些他认为没有获取成功的锁。
Redisson 实现方式(红锁 RedLock)
github Redisson https://github.com/redisson/r…
Maven
<!– JDK 1.8+ compatible –>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.9.0</version>
</dependency>

<!– JDK 1.6+ compatible –>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.14.0</version>
</dependency>
集群模式配置
集群模式除了适用于 Redis 集群环境,也适用于任何云计算服务商提供的集群模式,例如 AWS ElastiCache 集群版、Azure Redis Cache 和阿里云(Aliyun)的云数据库 Redis 版。
程序化配置集群的用法:
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
// 可以用 ”rediss://” 来启用 SSL 连接
.addNodeAddress(“redis://127.0.0.1:7000”, “redis://127.0.0.1:7001”)
.addNodeAddress(“redis://127.0.0.1:7002”);
return Redisson.create(config);
}
基于 Redis 的 Redisson 红锁 RedissonRedLock 对象实现了 Redlock 介绍的加锁算法。该对象也可以用来将多个 RLock 对象关联为一个红锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例。
RLock lock1 = redissonClient1.getLock(“lock1”);
RLock lock2 = redissonClient2.getLock(“lock2”);
RLock lock3 = redissonClient3.getLock(“lock3”);

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

lock.unlock();
Redisson 监控锁
大家都知道,如果负责储存某些分布式锁的某些 Redis 节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是 30 秒钟,也可以通过修改 Config.lockWatchdogTimeout 来另行指定。
另外 Redisson 还通过加锁的方法提供了 leaseTime 的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给 lock1,lock2,lock3 加锁,如果没有手动解开的话,10 秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待 100 秒时间,并在加锁成功 10 秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

lock.unlock();
往期精彩文章

Redis 服务器被攻击后该如何安全加固
MySQL 从删库到恢复,还用跑路吗?
理解 JWT 鉴权的应用场景及使用建议
浅谈偏向锁、轻量级锁、重量级锁
算法:一致性哈希算法的理解与实践
架构:通过案例读懂 RESTful 架构风格
架构:一文读懂 Apache Flink 架构及特性分析
架构:大数据推荐系统实时架构和离线架构
微服务:架构下静态数据通用缓存机制
微服务:小型系统如何“微服务”开发
微服务:深入理解为什么要设计幂等性的服务
中间件:应用消息中间件设计可以解决哪些实际问题?

退出移动版