前段时间,看到redis做者发布的一篇文章《Is Redlock safe?》,Redlock是redis做者基于redis设计的分布式锁的算法。文章原由是有一位分布式的专家写了一篇文章《How to do distributed locking》,质疑Redlock的正确性。redis做者则在《Is Redlock safe?》文章中给予回应,一来一回甚是精彩。文本就为读者一一解析两位专家的争论。html
在了解两位专家的争论前,让我先从我了解的分布式锁一一道来。文章中提到的分布式锁均为排他锁。java
我第一次接触分布式锁用的是mysql的锁表。当时我并无分布式锁的概念。只知道当时有两台交易中心服务器处理相同的业务,每一个交易中心处理订单的时候须要保证另外一个没法处理。因而用mysql的一张表来控制共享资源。表结构以下:node
CREATE TABLE `lockedOrder` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主码', `type` tinyint(8) unsigned NOT NULL DEFAULT '0' COMMENT '操做类别', `order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的order_id', `memo` varchar(1024) NOT NULL DEFAULT '', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_order_id` (`order_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的订单';
order_id记录了订单号,type和memo用来记录下是那种类型的操做锁定的订单,memo用来记录一下操做内容。这张表能完成分布式锁的主要缘由正是因为把order_id设置为了UNIQUE KEY
,因此同一个订单号只能插入一次。因而对锁的竞争就交给了数据库,处理同一个订单号的交易中心把订单号插入表中,数据库保证了只有一个交易中心能插入成功,其余交易中心都会插入失败。lock和unlock的伪代码也很是简单:mysql
def lock : exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo) if result == true : return true else : return false def unlock : exec sql: delete from lockedOrder where order_id='order_id'
读者能够发现,这个锁从功能上有几个问题:redis
这把锁没有过时时间,若是交易中心锁定了订单,但异常宕机后,这个订单就没法锁定了。这里为了让锁可以失效,须要在应用层加上定时任务,去删除过时还未解锁的订单。clear_timeout_lock的伪代码很简单,只要执行一条sql便可。算法
def clear_timeout_lock : exec sql : delete from lockedOrder where update_time < ADDTIME(NOW(),'-00:02:00')
这里设置过时时间为2分钟,也是从业务场景考虑的,若是订单处理时间可能超过2分钟的话,这个时候还须要加大。sql
这把锁是不能重入的,意思就是即便一个交易中心得到了锁,在它为解锁前,以后的流程若是有再去获取锁的话还会失败,这样就可能出现死锁。这个问题咱们当时没有处理,若是要处理这个问题的话,须要增长字段,在insert的时候,把该交易中心的标识加进来,这样再获取锁的时候, 经过select,看下锁定的人是否是本身。lock的伪代码版本以下:数据库
def lock : exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo) if result == true : return true else : exec sql : select id from lockedOrder where order_id='order_id' and memo = 'TradeCenterId' if count > 0 : return true else return false
在锁定失败后,看下锁是否是本身,若是是本身,那依然锁定成功。不过这个方法解锁又遇到了困难,第一次unlock就把锁给释放了,后面的流程都是在没锁的状况下完成,就可能出现其余交易中心也获取到这个订单锁,产生冲突。解决这个办法的方法就是给锁加计数器,记录下lock多少次。unlock的时候,只有在lock次数为0后才能删除数据库的记录。apache
能够看出,数据库锁能实现一个简单的避免共享资源被多个系统操做的状况。我之前在盛大的时候,发现盛大特别喜欢用数据库锁。盛大的前辈们会说,盛大基本上实现分布式锁用的都是数据库锁。在并发量不是那么恐怖的状况下,数据库锁的性能也不容易出问题,并且因为数据库的数据具备持久化的特性,通常的应用也足够应付。可是除了上面说的数据库锁的几个功能问题外,数据库锁并无很好的应付数据库宕机的场景,若是数据库宕机,会带来的整个交易中心没法工做。当时我也没想过这个问题,咱们整个交易系统,数据库是个单点,不过数据库实在是太稳定了,两年也没出过任何问题。随着工做经验的积累,构建高可用系统的概念愈来愈强,系统中是不容许出现单点的。如今想一想,经过数据库的同步复制,以及使用vip切换Master就能解决这个问题。编程
后来我开始接触缓存服务,知道不少应用都把缓存做为分布式锁,好比redis。使用缓存做为分布式锁,性能很是强劲,在一些不错的硬件上,redis能够每秒执行10w次,内网延迟不超过1ms,足够知足绝大部分应用的锁定需求。
redis锁定的原理是利用setnx命令,即只有在某个key不存在状况才能set成功该key,这样就达到了多个进程并发去set同一个key,只有一个进程能set成功。
仅有一个setnx命令,redis遇到的问题跟数据库锁同样,可是过时时间这一项,redis自带的expire功能能够不须要应用主动去删除锁。并且从 Redis 2.6.12 版本开始,redis的set命令直接直接设置NX和EX属性,NX即附带了setnx数据,key存在就没法插入,EX是过时属性,能够设置过时时间。这样一个命令就能原子的完成加锁和设置过时时间。
缓存锁优点是性能出色,劣势就是因为数据在内存中,一旦缓存服务宕机,锁数据就丢失了。像redis自带复制功能,能够对数据可靠性有必定的保证,可是因为复制也是异步完成的,所以依然可能出现master节点写入锁数据而未同步到slave节点的时候宕机,锁数据丢失问题。
redis做者鉴于单点redis做为分布式锁的可能出现的锁数据丢失问题,提出了Redlock算法,该算法实现了比单一节点更安全、可靠的分布式锁管理(DLM)。下面我就介绍下Redlock的实现。
Redlock算法假设有N个redis节点,这些节点互相独立,通常设置为N=5,这N个节点运行在不一样的机器上以保持物理层面的独立。
算法的步骤以下:
使用Redlock算法,能够保证在挂掉最多2个节点的时候,分布式锁服务仍然能工做,这相比以前的数据库锁和缓存锁大大提升了可用性,因为redis的高效性能,分布式缓存锁性能并不比数据库锁差。
介绍了Redlock,就能够提及文章开头提到了分布式专家和redis做者的争论了。
该专家提到,考虑分布式锁的时候须要考虑两个方面:性能和正确性。
若是使用高性能的分布式锁,对正确性要求不高的场景下,那么使用缓存锁就足够了。
若是使用可靠性高的分布式锁,那么就须要考虑严格的可靠性问题。而Redlock则不符合正确性。为何不符合呢?专家列举了几个方面。
如今不少编程语言使用的虚拟机都有GC功能,在Full GC的时候,程序会停下来处理GC,有些时候Full GC耗时很长,甚至程序有几分钟的卡顿,文章列举了HBase的例子,HBase有时候GC几分钟,会致使租约超时。并且Full GC何时到来,程序没法掌控,程序的任什么时候候均可能停下来处理GC,好比下图,客户端1得到了锁,正准备处理共享资源的时候,发生了Full GC直到锁过时。这样,客户端2又得到了锁,开始处理共享资源。在客户端2处理的时候,客户端1 Full GC完成,也开始处理共享资源,这样就出现了2个客户端都在处理共享资源的状况。
专家给出了解决办法,以下图,看起来就是MVCC,给锁带上token,token就是version的概念,每次操做锁完成,token都会加1,在处理共享资源的时候带上token,只有指定版本的token可以处理共享资源。
而后专家还说到了算法依赖本地时间,并且redis在处理key过时的时候,依赖gettimeofday方法得到时间,而不是monotonic clock,这也会带来时间的不许确。好比一下场景,两个客户端client 1和client 2,5个redis节点nodes (A, B, C, D and E)。
总结专家关于Redlock不可用的两点:
因此专家给出的结论是,只有在有界的网络延迟、有界的程序中断、有界的时钟错误范围,Redlock才能正常工做,可是这三种场景的边界又是没法确认的,因此专家不建议使用Redlock。对于正确性要求高的场景,专家推荐了Zookeeper,关于使用Zookeeper做为分布式锁后面再讨论。
redis做者看到这个专家的文章后,写了一篇博客予以回应。做者很客气的感谢了专家,而后表达出了对专家观点的不认同。
I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don’t agree with the analysis.
redis做者关于使用token解决锁超时问题能够归纳成下面五点:
专家说到的另外一个时钟问题,redis做者也给出了解释。客户端实际得到的锁的时间是默认的超时时间,减去获取锁所花费的时间,若是获取锁花费时间过长致使超过了锁的默认超时间,那么此时客户端并不能获取到锁,不会存在专家提出的例子。
看了两位专家你来我回的争辩,相信读者会对Redlock有了更多的认识。这里我也想就分布式专家提到的两个问题结合redis做者的观点,说说个人想法。
第一个问题我归纳为,在一个客户端获取了分布式锁后,在客户端的处理过程当中,可能出现锁超时释放的状况,这里说的处理中除了GC等非抗力外,程序流程未处理完也是可能发生的。以前在说到数据库锁设置的超时时间2分钟,若是出现某个任务占用某个订单锁超过2分钟,那么另外一个交易中心就能够得到这把订单锁,从而两个交易中心同时处理同一个订单。正常状况,任务固然秒级处理完成,但是有时候,加入某个rpc请求设置的超时时间过长,一个任务中有多个这样的超时请求,那么,极可能就出现超过自动解锁时间了。当初咱们的交易模块是用C++写的,不存在GC,若是用java写,中间还可能出现Full GC,那么锁超时解锁后,本身客户端没法感知,是件很是严重的事情。我以为这不是锁自己的问题,上面说到的任何一个分布式锁,只要自带了超时释放的特性,都会出现这样的问题。若是使用锁的超时功能,那么客户端必定得设置获取锁超时后,采起相应的处理,而不是继续处理共享资源。Redlock的算法,在客户端获取锁后,会返回客户端能占用的锁时间,客户端必须处理该时间,让任务在超过该时间后中止下来。
第二个问题,天然就是分布式专家没有理解Redlock。Redlock有个关键的特性是,获取锁的时间是锁默认超时的总时间减去获取锁所花费的时间,这样客户端处理的时间就是一个相对时间,就跟本地时间无关了。
由此看来,Redlock的正确性是能获得很好的保证的。仔细分析Redlock,相比于一个节点的redis,Redlock提供的最主要的特性是可靠性更高,这在有些场景下是很重要的特性。可是我以为Redlock为了实现可靠性,却花费了过大的代价。
分析了这么多缘由,我以为Redlock的问题,最关键的一点在于Redlock须要客户端去保证写入的一致性,后端5个节点彻底独立,全部的客户端都得操做这5个节点。若是5个节点有一个leader,客户端只要从leader获取锁,其余节点能同步leader的数据,这样,分区、超时、冲突等问题都不会存在。因此为了保证分布式锁的正确性,我以为使用强一致性的分布式协调服务能更好的解决问题。
提到分布式协调服务,天然就想到了zookeeper。zookeeper实现了相似paxos协议,是一个拥有多个节点分布式协调服务。对zookeeper写入请求会转发到leader,leader写入完成,并同步到其余节点,直到全部节点都写入完成,才返回客户端写入成功。
zookeeper还有几个特质,让它很是适合做为分布式锁服务。
zookeeper实现锁的方式是客户端一块儿竞争写某条数据,好比/path/lock,只有第一个客户端能写入成功,其余的客户端都会写入失败。写入成功的客户端就得到了锁,写入失败的客户端,注册watch事件,等待锁的释放,从而继续竞争该锁。
若是要实现tryLock,那么竞争失败就直接返回false便可。
zookeeper实现的分布式锁简单、明了,分布式锁的关键技术都由zookeeper负责实现了。能够看下《从Paxos到Zookeeper:分布式一致性原理与实践》书里贴出来的分布式锁实现步骤
须要使用zookeeper的分布式锁功能,可使用curator-recipes库。Curator是Netflix开源的一套ZooKeeper客户端框架,curator-recipes库里面集成了不少zookeeper的应用场景,分布式锁的功能在org.apache.curator.framework.recipes.locks包里面,《跟着实例学习ZooKeeper的用法: 分布式锁》文章里面详细的介绍了curator-recipes分布式锁的使用,想要使用分布式锁功能的朋友们不妨一试。
文章写到这里,基本把我关于分布式锁的了解介绍了一遍。能够实现分布式锁功能的,包括数据库、缓存、分布式协调服务等等。根据业务的场景、现状以及已经依赖的服务,应用可使用不一样分布式锁实现。文章介绍了redis做者和分布式专家关于Redlock,虽然最终以为Redlock并不像分布式专家说的那样缺少正确性,不过我我的以为,若是须要最可靠的分布式锁,仍是使用zookeeper会更可靠些。curator-recipes库封装的分布式锁,java应用也能够直接使用。并且若是开始依赖zookeeper,那么zookeeper不只仅提供了分布式锁功能,选主、服务注册与发现、保存元数据信息等功能都能依赖zookeeper,这让zookeeper不会那么闲置。
参考资料: