分布式锁之主从架构数据同步异常问题

分布式锁之主从架构数据同步异常问题

背景

我们平常使用的分布式锁在Redis单节点情况下是可以正常使用,数据准确的。

我们会在此加上很多限制,比如避免解锁其他线程、避免服务器宕机所设置超时时间等。

而我们项目中,为了保证Redis高可用,一般都会使用「Redis Cluster」或者「哨兵模式」这两种模式。而Redis的异步数据同步就可能会造成分布式锁失效问题,本文主要讨论在分布式架构上的数据安全问题。

单节点失效原因

这边用网上的一张图来解释,为什么会锁失效?

  1. 在Master节点请求锁。
  2. Master写入成功并返回OK。
  3. Master还未同步到Slave时候挂了,Slave节点选举为Master。
  4. 锁数据丢失。

图片

Redlock算法

针对上面的问题,Redis 之父 antirez 设计了 Redlock 算法,Redlock 的算法描述就放在 Redis 的官网上:

  • https://redis.io/topics/distlock

在 Redlock 之前,很多人对于分布式锁的实现都是基于单个 Redis 节点的。而 Redlock 是基于多个 Redis 节点(都是 Master)的一种实现。前面基于单 Redis 节点的算法是 Redlock 的基础。

1.什么是Redlock?

Redlock 算法基于 N 个完全独立的 Redis 节点,客户端依次执行下面各个步骤,来完成获取锁的操作:

  1. 获取当前时间 T1(毫秒数)。

  2. 向所有Redis节点依次加锁。

    (使用相同的 key、value 按顺序依次向 N 个 Redis 节点执行获取锁的操作。这个获取操作跟前面基于单 Redis 节点的获取锁的过程相同,包含随机字符串 my_random_value,也包含过期时间(比如 PX 30000,即锁的有效时间)。为了保证在某个 Redis 节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个 Redis 节点获取锁失败以后,应该立即尝试下一个 Redis 节点。)

  3. 多半节点成功获取锁,且加锁时间不超过锁的有效时间。

    获取当前时间 T2 减去步骤 1 中的 T1,计算获取锁消耗了多长时间(T3= T2-T1),计算方法是用当前时间减去第 1 步记录的时间。如果客户端从大多数 Redis 节点(大于等于 N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。

  4. 锁的有效时间要减去加锁花费时间

    如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第 3 步计算出来的获取锁消耗的时间

  5. 加锁失败需要将加锁成功的节点解锁

    如果最终获取锁失败了(可能由于获取到锁的 Redis 节点个数少于 N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有 Redis 节点发起释放锁的操作

注意:⚠️我们平时操作Redis是对主节点进行增删,但是我们加锁是直接和集群各个节点通信获取锁。

2.Redlock为什么要这样做

问题 1:为什么要在多个实例上加锁?

本质就是保证分布式锁系统高可用,单节点有可能崩溃后数据丢失。而多实例加锁,能够最大限度避免Redis服务数据丢失问题,部分实例异常宕机,剩余实例只要超过 N/2+1 依旧可用。

问题 2:为什么步骤 3 加锁成功之后,还要计算加锁的累计耗时?

因为是对所有节点加锁,其中需要每个节点的网络来回,请求多延迟、丢包概率就高。如果累计耗时超过锁的存在时间,那么此时锁没有任何意义了,相当于已经解锁了。

问题 3:为什么释放锁,要操作所有节点,对所有节点都释放锁?

因为当对某一个 Redis 节点加锁时,可能因为网络原因导致加锁“失败”。注意这个“失败”,指的是 Redis 节点实际已经加锁成功了,但是返回的结果因为网络延迟并没有传到加锁的线程,被加锁线程丢弃了,加锁线程误以为没有成功,于是加锁线程去尝试下一个节点了。

所以释放锁的时候,不管以前有没有加锁成功,都要释放所有节点的锁,以保证清除节点残留的锁。

3.Redlock就完美解决了吗?

1.服务器崩溃

考虑一种情况,如果我们刚好只对其中“大多数”加锁成功了。

如下图:Redis1、Redis2、Redis3成功了

图片

这个时候Redis3宕机了,刚好发生了单节点数据丢失的问题了,恢复后发现Redis3、Redis4、Redis5被其他线程加锁成功了。锁失效了。

如果配置了AOF持久化策略采用了fsync=always,保存重启后不会丢失,但这样性能大大降低,不符合高性能的标准。

-我们可以把重启时间人为控制,超过锁的最久超时时间即可,只要锁都超时,就能保证没有同时有两个线程加锁成功情况了。

2.客户端长期阻塞导致锁过期

我们来看一张图,在线程1获取到锁后发生了很长时间的STW,这个时候锁自动过时了,被线程2获取了,那么锁过期了。

图片

解决方案——fencing token

fencing token 是一个**单调递增的数字,**当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个 fencing token,这样提供共享资源的服务就能根据它进行检查,拒绝掉延迟到来的访问请求(避免了冲突)。

图片

在上图中,客户端 1 先获取到的锁,因此有一个较小的 fencing token,等于 33,而客户端 2 后获取到的锁,有一个较大的 fencing token,等于 34。客户端 1 从 GC pause 中恢复过来之后,依然是向存储服务发送访问请求,但是带了 fencing token = 33。存储服务发现它之前已经处理过 34 的请求,所以会拒绝掉这次 33 的请求。这样就避免了冲突。

3.时间跳跃

构造了一些事件序列,能够让 Redlock 失效(两个客户端同时持有锁)。为了说明 Redlock 对系统记时(timing)的过分依赖,首先给出了下面的一个例子(还是假设有 5 个 Redis 节点 A, B, C, D, E):

  1. 客户端 1 从 Redis 节点 A, B, C 成功获取了锁(多数节点)。由于网络问题,与 D 和 E 通信失败。
  2. 节点 C 上的时钟发生了向前跳跃(比如人为修改时间),导致它上面维护的锁快速过期。
  3. 客户端 2 从 Redis 节点 C, D, E 成功获取了同一个资源的锁(多数节点)。
  4. 客户端 1 和客户端 2 现在都认为自己持有了锁。

本质上是因为Redlock 的安全性(safety property)对系统的时钟有比较强的依赖,一旦系统的时钟变得不准确

一个好的分布式算法,这些因素不应该影响它的安全性(safety property),只可能影响到它的活性(liveness property)

也就是说,即使在非常极端的情况下(比如系统时钟严重错误),算法顶多是不能在**有限的时间内给出结果而已,而不应该给出错误的结果。这样的算法在现实中是存在的,像比较著名的Paxos,或 Raft。**但显然按这个标准的话,Redlock 的安全性级别是达不到的。

3.1 时钟变迁如何解决

为什么系统时钟会存在迁移

linux 提供了两个系统时间:clock realtime 和 clock monotonic

  • clock realtime 也就是 xtime/wall time,这个时间是可以被用户改变的,被 NTP 改变。Redis 的判断超时使用的 gettimeofday 函数取的就是这个时间,Redis 的过期计算用的也是这个时间。

    参考https://blog.habets.se/2010/09/gettimeofday-should-never-be-used-to-measure-time.html

  • clock monotonic,直译过来是单调时间,不会被用户改变,但是会被 NTP 改变。

clock realtime 可以被人为修改,在实现分布式锁时,不应该使用 clock realtime。不过很可惜,Redis 使用的就是这个时间,Redis 5.0 使用的还是 clock realtime。Redis作者说过后面会改成 clock monotonic 的。也就是说,人为修改 Redis 服务器的时间,就能让 Redis 出问题了。

发生时钟迁移情况

  • 人为修改了时钟
  • 从 NTP 服务收到了一个大的时钟更新事件导致时钟漂移
  • 闰秒(是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中或者季末对协调世界时增加或减少 1 秒的调整,此时一分钟为 59 秒或者 61 秒,闰秒曾使许多大型系统崩溃)

如何解决

  1. Fencing token 机制:类似 raft 算法、zab 协议中的全局递增数字,对这个 token 的校验需要后端资源进行校验,如此一来,相当于后端资源具备了互斥机制,而且涉及到后端资源的改造。

总结

Redlock在Redis集群可以解决大部分发生的,数据不安全的问题,但并不是完全解决,因为在极端情况下还是会出现服务器时间过期等情况。其核心的问题为:缺乏锁数据丢失的识别和感知机制。

RedLock 中的每台 Redis,充当的仍旧只是分布式存储锁数据的功能,每台 Redis 之间各自独立,单台 Redis 缺乏全局的信息,自然也不知道自己的锁数据是否是完整的。在单台 Redis 数据的不完整的前提下,没有分布式共识机制, 使得在各种分布式环境的典型场景下(结点故障、网络丢包、网络乱序),没有完整数据但参与决策,从而破坏数据一致性。

转载自: https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw

end
  • 作者:Endwas(联系作者)
  • 发表时间:2022-08-15 15:57
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者名字和博客地址
  • 评论