RedLock分布式锁很多人用过,但是大家是否知道,这个算法出来的时候关于他的正确性发生过一些争论,首先是Martin Kleppmann发表了一篇质疑文章,然后antirez做了回应,我们看一下大佬的争论。让我们直击大佬对分布式锁算法及分布式系统设计的探讨和思考

Martin Kleppmann关于RedLock的质疑

原文: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed...

分布式锁的作用:

1、效率:如果你的程序有大量耗时操作,通过分布式锁来控制避免重复执行。如果多执行了一次,所造成的影响只是多浪费一点计算资源(多做了一次耗时的计算),或者造成一点不方便(比如客户收到重复的两个邮件)

2、正确性:通过锁来控制并发操作避免造成混乱,如果锁控制失败两个节点并行,可能造成数据丢失,文件损坏等严重问题。

  • 针对效率:如果仅仅是为了效率使用锁,只使用一个Redis节点加锁就可以,没有必要RedLock复杂算法。
  • 针对正确性:RedLock使用5个独立节点加锁并获得大部分锁的算法适用于你对程序正确性要求很严格的场景,但是这个算法也不能保证锁的正确性。

下面阐述一下存在的问题:

问题一、锁的自动超时的问题(分布式锁的共同问题):

假设你有一个应用程序需要修改共享存储系统的文件,使用锁的伪代码如下:

// THIS CODE IS BROKEN
function writeData(filename, data) {
    var lock = lockService.acquireLock(filename);
    if (!lock) {
        throw 'Failed to acquire lock';
    }

    try {
        var file = storage.readFile(filename);
        var updated = updateContents(file, data);
        storage.writeFile(filename, updated);
    } finally {
        lock.release();
    }
}

但是仍会出现问题,如图:
image

Martin这里说的是锁的超时时间引起的问题:

  • Client1 获取锁,开始执行,但是这是发生FGC,线程停顿了很长时间,中间锁超时
  • Client2 获得锁,开始执行,并将结果写入Storage
  • Client1 FGC结束,将他的结果写入Storage,覆盖了Client2的写入,发生问题

那怎么解决呢:

如果Client1 FGC结束后,从Storage查询文件,检查文件是否和自己修改前一致再决定是否写入行不行?或者说FGC结束后,Client1判断一下它持有锁的时间是否超时行不行?

答案是否定的,因为FGC可以发生在任意时间,在判断语句执行后,写入Storage前也可能发生FGC。

那我们换一个没有GC的语言可以吗?
也不行,因为GC是其中一个原因,网络波动、IO操作、进程时间片结束等等也可能导致该问题

PS:这个问题通过zookeeper锁可以解决,zookeeper锁是通过和客户端保持一个链接来保证所的有效性的,如果客户端没有执行完成,锁一直有效

但是Martin提出了另外一个解决该问题的办法:
给锁增加一个token,每次获取锁token增加1,token是单调递增的

  • Client1 获取锁,token=33,开始执行,但是这是发生FGC,线程停顿了很长时间,中间锁超时
  • Client2 获得锁,token=34,开始执行,并将结果写入Storage
  • Client1 FGC结束,将他的结果写入Storage,Storage判断Client1的token比上一次写入的小,拒绝这次写入操作

PS: 这个方法本质上是存储系统来控制的乐观锁,存储系统需要记录上一次操作共享资源的token值,并把这次和上一次的对比,如果相同可以修改,否则拒绝。那么我们是不是可以在存储系统读取数据的时候拿到这个值,不需要再通过分布式锁来获取了。
在关系数据库,如Mysql,我们可以通过在每条记录中增加version字段作为乐观锁字段使用

问题二、RedLock还依赖系统时钟

RedLock依赖很多对系统时间的假设:
1)所有的Redis节点锁超时的时间长度是一样的
2)网络延迟对锁的过期时间来说可以忽略
3)进程暂停比锁的过期时间小很多,可以忽略,

但是上述假设并不成立,看一个例子,Redis(A,B,C,D,E)五个独立节点和两个客户端(Client1,Client2)

  • Client1 在A、B、C、D、E上获取锁,由于网络延迟,D、E未收到请求
    C的时钟向前跳,C的锁过期
  • Client2在C、D、E上获取锁,由于网络延迟A、B位收到请求
  • Client1和Client2同时持有锁

如果C把key持久化之前发生重启,也会发生上述情况,RedLock文档建议延迟启动宕机机器,延迟时间至少超过最长的锁超时时间,但是这同样依赖精确的时间测量,如果时钟发生跳跃就失败了。

如果你认为时钟跳跃不现实,那么下面进程暂停的例子:

  • Client1 从A、B、C、D、E请求锁
  • 请求的响应还在网络上传输的时候,Client1 发生FGC
  • 锁在所有的节点上超时
  • Client2从五个节点获得锁
  • Client1结束FGC并收到响应获得锁,两个客户端同时持有锁

在这里Martin总结,RedLock正确工作依赖一个同步系统模型,即满足以下三点:

  • 有限的网络延迟(网络延迟有一个最大时间,github 90秒数据包延迟案例90-second packet delay)
  • 有限的进程暂停(只有汽车的安全气囊才可以做到)
  • 有限的时钟错误(祈祷你不是从坏的NTP获取时间)
Martin提出了分布式系统设计的要点:
在异步模型(即网络延迟,时钟错误、程序暂停等等没有限制)下,分布式系统设计不能依赖任何时间假设(即网络延迟、时钟错误、程序暂停等有限制),这些问题只能影响系统活性。换句话说,即使系统发生所有这些问题(程序暂停、网络延迟、时钟向前或者向后跳跃),算法的性能可能受到影响,但是永远不会发生错误。

这里总结一下Martin指出了RedLock两个问题

  • 一个是锁带有超时时间引起的问题
  • 一个是RedLock依赖一个同步系统模型

Martin总结对RedLock的批评

  • 针对效率,RedLock算法太复杂耗时
  • 针对正确性,RedLock又不是足够安全

antirez的回应

原文:http://antirez.com/news/101

首先antirez总结的Martin说的两个问题:

  • 1、RedLock的超时机制没办法保证锁的互斥
  • 2、这个算法依赖一些系统模型的假设,现实是这些假设无法保证
针对第一个问题

antirez说了几点:

  • 1、antirez认为分布式锁是在你没有其他办法控制共享资源互斥访问的时候使用的,如果你已经有了办法保证严格的同步互斥,那你为什么还用分布式锁

    ps:这里应该是指Martin提出获取锁增加Fencing Token来控制,我在上面讲过,这种方式本质上是存储系统的乐观锁
  • 2、第二点,antirez说如果你非要这么干,那么可以给每个RedLock生成一个自增ID,但是他下面认为没必要这么做

    ps:这里并没有说具体如何实现给每个RedLock生成一个自增ID
  • 3、接着说每个RedLock都有一个随机的Token(20字节,可以保证不会出现相同的),可以用这个当Token,然后自己去实现检查Token再写入的操作

    ps:按道理需要存储系统来实现Token的检查和写入操作,才能保证同步互斥,这里没有说如何实现
  • 4、这里又说顺序Token没必要,因为GC暂停,客户端获取Token的顺序可能和他们想要操作共享资源的顺序不一样

    ps:这里我不赞同,谁能优先操作共享资源,应该按谁先获取锁(即Token)来决定

antirez说了这么多,但是没有解决我的疑问。

针对第二个问题

antirez的解释:

系统模型

首先说这个问题是所有带超时机制的分布式锁都存在的。

Martin谈到的时钟随机跳跃有以下两个原因:
1、管理员修改
2、从ntpd服务获取一个跳跃的时间

第一个问题可以让管理员不要修改时间。
第二个问题可以使用一个可靠的ntpd,它不通过跳跃来修改时间,而是分配到一个更大的时间跨度内修改。

如果我们使用操作系统提供的单调递增的时间API,那么进程完全可以将时间计算的误差控制在一个较小的范围内。

网络延迟

Martin的描述包含以下三点:
1、 Redis的所有节点存储key的时间长度是大约一直的
2、 网络延迟相对过期时间很小
3、 程序暂停相对过期时间非常小

简单起见,假设我们使用系统提供的单调递增的时间API:

关于1:这不是个问题,我们计算时间的速度大体一致
关于2:这个问题有点麻烦,
查看RedLock规范,回顾一下获取锁的步骤:

  • 1、获取当前时间
  • 2、...从所有的节点上获取锁的步骤...
  • 3、获取当前时间
  • 4、检查是否超时
  • 5、锁没有超时,执行逻辑

从第一步到第三步,不论发生什么样的网络延迟,都可以感知,不会出问题。
关键是第三步后的延迟,比如程序暂停(针对第三点),会影响到客户端是否能在锁有效时间内完成工作,这是所有带超时机制的分布式锁都存在的问题

这里阐述了这个问题的没办法解决

总结

根据Martin和antirez对分布式锁的讨论,我们知道,由于分布式系统的各种问题程序暂停、系统时间跳跃、网络延迟等问题导致锁的基本特性互斥性实现起来非常困难。

分布式锁必须在客户端出故障的时候能够被释放,同时要保证锁的互斥性,那么有两种措施:

  • 1)设置一个超时时间

    这种方案可能会存在这个问题,客户端没有执行完成锁已经过期并被其他客户端获取,那么在事务提交前需要判断锁是否超时,这又因为GC或者网络原因导致在判断后到事务提交前这段时间内锁状态可能已经变化,锁由未超时变成超时又被其他客户端获取,违反了互斥性。

  • 2)监听客户端的状态

    zookeeper临时节点不需要设置超时时间,客户端故障了会自动删除,但是zookeeper和客户端保持连接的方式是通过定时的心跳,客户端发送的心跳可能因为网络延迟,zookeeper没有收到心跳而释放锁并被其他客户端获取,但是客户端可能不知道锁一倍而继续执行,这又违反了互斥性。

存在上述两个问题的原因主要是锁系统和存储系统分开了,这中间的时差导致用这种方案永远没法保证锁的安全。
唯一绝对保证锁的安全的做法就是存储系统来实现锁的能力,在关系数据库就是数据库的乐观锁才能保证锁的互斥性。其他存储系统可以参考Martin的Token方案。

分布式系统设计很复杂,每种系统都有其设计的取舍,我们要使用就要了解系统的优缺点和其设计的目标,才能充分发挥其作用,避免其问题。

对于分布式锁,单个Redis节点实现锁、RedLock、Zookeeper锁了解其优缺点,根据需求选择即可。

单个Redis实现的锁就能满足大部分需求,实际上现在的项目大部分Redis是集群部署,对外单一镜像,使用RedLock相当于单个Redis实现的锁。

如何选择锁

那么根据Martin提出的锁的目标,你将根据需要使用锁,结论就是:

  • 针对效率,可以使用单个Redis加锁的方法(Redission实现的RedLock可以直接使用),或者zookeeper锁
  • 针对正确性,可以使用数据库乐观锁

参考资料:

RedLock锁规范 Distributed locks with Redis
Martin对RedLock的质疑 How to do distributed locking
antirez对Martin的回应 Is Redlock safe?


杜若
70 声望3 粉丝