Redis分布式锁并不适合项目应用(打脸)

昨天,分享了一篇Redis实现分布式锁服务的文章,有单节点的分布式锁实现和红锁的实现两种。大家都知道单节点存在单点故障的问题,所以引入了红锁的实现。但红锁真的就可靠吗?这个问题,本应该在昨天文章中提出的,这里先打脸,给大家道歉。今天我们过一遍Martin的文章,分析基于Redis实现的分布式锁服务到底有哪些问题。

Redis分布式锁并不适合项目应用(打脸)

目录:

  1. 我们使用锁的目的

  2. 用锁保护共享资源

  3. 具有版本的锁服务

  4. 红锁不可靠的示例

1. 我们使用锁的目的

并发操作共同的资源容易引起数据更新丢失,导致脏读、数据不一致,所以需要使用锁保护起来。应用中,我们把目标的实现定义为两种:一种是可以容忍概率极低的稍许错误,因为错误的代价可以忽略;另外一种是绝对可靠,不容许任何错误,任何错误都是致命的。在搞清楚目的之后,选择锁的实现上,就可以有效做出取舍。如果可已容忍稍许错误,选择Redis实现的锁完全没有问题。否则,不建议使用。下面说明一下为什么不建议使用。

2. 用锁保护共享资源

抛开红锁的算法,我们先看一个例子:文件并发读写问题,首先有一个client获得了锁,读取并修改了文件,然后写到存储系统中,并最后释放了锁。该锁防止其他client并发修改,代码示例:

Redis分布式锁并不适合项目应用(打脸)

以上代码看上去没有什么问题,但是在一个分布式系统中,以上代码很可能导致数据更新丢失,导致数据损坏:

Redis分布式锁并不适合项目应用(打脸)

  1. client1获得了锁,过程中JVM垃圾回收,线程阻塞导致锁超时;

  2. client2获得锁,更新完成写操作,释放锁。

  3. client1线程恢复,写到存储。

以上发生了同一时间,有两个线程同时获得锁,导致数据更新丢失。在JAVA代码中,垃圾回收是不可预知的,随时都可能发生。即使我们不使用JAVA编程,排除垃圾回收的问题,那么我们仍然不可预知是否存在其他因素导致线程暂停,例如:从硬盘加载数据到内存、网络拥塞期间、报文网络传输包丢失、CPU时间分片期间、操作系统中断等等因素,都会导致程序线程被阻塞暂停,因此我们不可预知客户端是否能在锁超时之前完成更新操作。

3. 具有版本的锁服务

要想实现一个可靠的分布式锁服务实际上很简单,我们在完成写操作的时候,带上读取时的版本号作为更新条件。该版本号可以是递增序列,在获得锁的时候+1。

Redis分布式锁并不适合项目应用(打脸)

  1. Client1在获得锁的时候,分配了一个token=33,之后线程暂停直至锁超时。

  2. Client2获得锁,分配了token=34,完成更新,成功落盘,然后结合token=34成功落盘。

  3. Client1恢复,写盘发现token=33已经失效,锁服务抛异常,拒绝本次提交。

然而RedLock算法并不具备生成类似版本号的功能,即便算法再怎么完备,也不能防止获得资源锁的线程被其他不可预知的因素阻塞,从而导致锁超时。

4. 红锁不可靠的示例

分析下为什么RedLock不可靠:依赖Redis时间的锁不可靠(clock存在时间跳变)。

示例:5个Redis节点,2个client,网络跳变导致锁失效。

  1. Client1获得A、B、C锁,因为网络问题,D、E不可达。

  2. 时钟跳变,导致C锁超时。

  3. Client2获得C、D、E锁,因为网络问题,A、B不可达。

  4. Client1和Client2同时持有相同的锁。

即使我们假设不存在时间跳变,完美的NTP校时服务,依然会有问题:

  1. Client1请求节点A、B、C、D、E的锁。

  2. Client1在收到请求返回之前,线程暂停了。

  3. Client1在节点上的锁过期了。

  4. Client2获得了A、B、C、D、E上的锁。

  5. Client1恢复并收到了A、B、C、D、E的回复内容(Socket Rec Buffer内的缓存),成功获得锁。

  6. Client1和Client2同时持有相同的锁。

分析下为什么RedLock不可靠:基于同步编程模型的RedLock不可靠。

使用同步的编程模型,我们必须有以下假定:假设有可控的网络延迟、假设有可控的线程调度、假设有可控的时钟异常。

只有在以上假定的条件下,我们使用红锁才可以使用补偿的方式解决网络延迟、时钟漂移、线程调度的问题。红锁假定了网络延迟、时钟漂移以及线程调度的时间非常短,远小于锁超时时间。一旦补偿的时间和锁过期时间相同了,红锁算法就会失效。

在理想的环境中,大多数情况下是可以满足的,也正是因此我们产生了大多数情况下满足条件的一致性算法,例如Zab和Paxos。

总结:

  1. 不建议使用红锁,实现起来重,而且不可靠。

  2. 建议使用基于版本的分布式锁实现,例如使用ZK、Curator、甚至使用数据的事务实现锁。

  3. 如果对锁的要求不是那么严格,完全可以使用单节点Redis实现的锁。

  4. 昨天有网友提出很不错,本文就有作者明确的立场:请发挥Redis的优势,而非短板。

  5. 我们马上玩转ZK,别急。

大家有想学习的东西,不妨在评论中提出,希望能帮到大家,给大家带来收获。


分享到:


相關文章: