昨天,分享了一篇Redis实现分布式锁服务的文章,有单节点的分布式锁实现和红锁的实现两种。大家都知道单节点存在单点故障的问题,所以引入了红锁的实现。但红锁真的就可靠吗?这个问题,本应该在昨天文章中提出的,这里先打脸,给大家道歉。今天我们过一遍Martin的文章,分析基于Redis实现的分布式锁服务到底有哪些问题。
目录:
我们使用锁的目的
用锁保护共享资源
具有版本的锁服务
红锁不可靠的示例
1. 我们使用锁的目的
并发操作共同的资源容易引起数据更新丢失,导致脏读、数据不一致,所以需要使用锁保护起来。应用中,我们把目标的实现定义为两种:一种是可以容忍概率极低的稍许错误,因为错误的代价可以忽略;另外一种是绝对可靠,不容许任何错误,任何错误都是致命的。在搞清楚目的之后,选择锁的实现上,就可以有效做出取舍。如果可已容忍稍许错误,选择Redis实现的锁完全没有问题。否则,不建议使用。下面说明一下为什么不建议使用。
2. 用锁保护共享资源
抛开红锁的算法,我们先看一个例子:文件并发读写问题,首先有一个client获得了锁,读取并修改了文件,然后写到存储系统中,并最后释放了锁。该锁防止其他client并发修改,代码示例:
以上代码看上去没有什么问题,但是在一个分布式系统中,以上代码很可能导致数据更新丢失,导致数据损坏:
client1获得了锁,过程中JVM垃圾回收,线程阻塞导致锁超时;
client2获得锁,更新完成写操作,释放锁。
client1线程恢复,写到存储。
以上发生了同一时间,有两个线程同时获得锁,导致数据更新丢失。在JAVA代码中,垃圾回收是不可预知的,随时都可能发生。即使我们不使用JAVA编程,排除垃圾回收的问题,那么我们仍然不可预知是否存在其他因素导致线程暂停,例如:从硬盘加载数据到内存、网络拥塞期间、报文网络传输包丢失、CPU时间分片期间、操作系统中断等等因素,都会导致程序线程被阻塞暂停,因此我们不可预知客户端是否能在锁超时之前完成更新操作。
3. 具有版本的锁服务
要想实现一个可靠的分布式锁服务实际上很简单,我们在完成写操作的时候,带上读取时的版本号作为更新条件。该版本号可以是递增序列,在获得锁的时候+1。
Client1在获得锁的时候,分配了一个token=33,之后线程暂停直至锁超时。
Client2获得锁,分配了token=34,完成更新,成功落盘,然后结合token=34成功落盘。
Client1恢复,写盘发现token=33已经失效,锁服务抛异常,拒绝本次提交。
然而RedLock算法并不具备生成类似版本号的功能,即便算法再怎么完备,也不能防止获得资源锁的线程被其他不可预知的因素阻塞,从而导致锁超时。
4. 红锁不可靠的示例
分析下为什么RedLock不可靠:依赖Redis时间的锁不可靠(clock存在时间跳变)。
示例:5个Redis节点,2个client,网络跳变导致锁失效。
Client1获得A、B、C锁,因为网络问题,D、E不可达。
时钟跳变,导致C锁超时。
Client2获得C、D、E锁,因为网络问题,A、B不可达。
Client1和Client2同时持有相同的锁。
即使我们假设不存在时间跳变,完美的NTP校时服务,依然会有问题:
Client1请求节点A、B、C、D、E的锁。
Client1在收到请求返回之前,线程暂停了。
Client1在节点上的锁过期了。
Client2获得了A、B、C、D、E上的锁。
Client1恢复并收到了A、B、C、D、E的回复内容(Socket Rec Buffer内的缓存),成功获得锁。
Client1和Client2同时持有相同的锁。
分析下为什么RedLock不可靠:基于同步编程模型的RedLock不可靠。
使用同步的编程模型,我们必须有以下假定:假设有可控的网络延迟、假设有可控的线程调度、假设有可控的时钟异常。
只有在以上假定的条件下,我们使用红锁才可以使用补偿的方式解决网络延迟、时钟漂移、线程调度的问题。红锁假定了网络延迟、时钟漂移以及线程调度的时间非常短,远小于锁超时时间。一旦补偿的时间和锁过期时间相同了,红锁算法就会失效。
在理想的环境中,大多数情况下是可以满足的,也正是因此我们产生了大多数情况下满足条件的一致性算法,例如Zab和Paxos。
总结:
不建议使用红锁,实现起来重,而且不可靠。
建议使用基于版本的分布式锁实现,例如使用ZK、Curator、甚至使用数据的事务实现锁。
如果对锁的要求不是那么严格,完全可以使用单节点Redis实现的锁。
昨天有网友提出很不错,本文就有作者明确的立场:请发挥Redis的优势,而非短板。
我们马上玩转ZK,别急。
大家有想学习的东西,不妨在评论中提出,希望能帮到大家,给大家带来收获。
閱讀更多 吳濤分享 的文章