聊一聊分布式锁的设计

原由

前段时间,看到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

  • 数据库锁实现只能是非阻塞锁,即应该为tryLock,是尝试得到锁,若是没法得到则会返回失败。要改为阻塞锁,须要反复执行insert语句直到插入成功。因为交易中心的使用场景,只要一个交易中心处理订单就好了,因此这里不须要使用阻塞锁。
  • 这把锁没有过时时间,若是交易中心锁定了订单,但异常宕机后,这个订单就没法锁定了。这里为了让锁可以失效,须要在应用层加上定时任务,去删除过时还未解锁的订单。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节点的时候宕机,锁数据丢失问题。

分布式缓存锁—Redlock

redis做者鉴于单点redis做为分布式锁的可能出现的锁数据丢失问题,提出了Redlock算法,该算法实现了比单一节点更安全、可靠的分布式锁管理(DLM)。下面我就介绍下Redlock的实现。

Redlock算法假设有N个redis节点,这些节点互相独立,通常设置为N=5,这N个节点运行在不一样的机器上以保持物理层面的独立。

算法的步骤以下:

  • 一、客户端获取当前时间,以毫秒为单位。
  • 二、客户端尝试获取N个节点的锁,(每一个节点获取锁的方式和前面说的缓存锁同样),N个节点以相同的key和value获取锁。客户端须要设置接口访问超时,接口超时时间须要远远小于锁超时时间,好比锁自动释放的时间是10s,那么接口超时大概设置5-50ms。这样能够在有redis节点宕机后,访问该节点时能尽快超时,而减少锁的正常使用。
  • 三、客户端计算在得到锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端得到了超过3个节点的锁,并且获取锁的时间小于锁的超时时间,客户端才得到了分布式锁。
  • 四、客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。
  • 五、若是客户端获取锁失败了,客户端会依次删除全部的锁。

使用Redlock算法,能够保证在挂掉最多2个节点的时候,分布式锁服务仍然能工做,这相比以前的数据库锁和缓存锁大大提升了可用性,因为redis的高效性能,分布式缓存锁性能并不比数据库锁差。

分布式专家质疑Redlock

介绍了Redlock,就能够提及文章开头提到了分布式专家和redis做者的争论了。

该专家提到,考虑分布式锁的时候须要考虑两个方面:性能和正确性。

若是使用高性能的分布式锁,对正确性要求不高的场景下,那么使用缓存锁就足够了。

若是使用可靠性高的分布式锁,那么就须要考虑严格的可靠性问题。而Redlock则不符合正确性。为何不符合呢?专家列举了几个方面。

如今不少编程语言使用的虚拟机都有GC功能,在Full GC的时候,程序会停下来处理GC,有些时候Full GC耗时很长,甚至程序有几分钟的卡顿,文章列举了HBase的例子,HBase有时候GC几分钟,会致使租约超时。并且Full GC何时到来,程序没法掌控,程序的任什么时候候均可能停下来处理GC,好比下图,客户端1得到了锁,正准备处理共享资源的时候,发生了Full GC直到锁过时。这样,客户端2又得到了锁,开始处理共享资源。在客户端2处理的时候,客户端1 Full GC完成,也开始处理共享资源,这样就出现了2个客户端都在处理共享资源的状况。

Alt text

专家给出了解决办法,以下图,看起来就是MVCC,给锁带上token,token就是version的概念,每次操做锁完成,token都会加1,在处理共享资源的时候带上token,只有指定版本的token可以处理共享资源。

Alt text

而后专家还说到了算法依赖本地时间,并且redis在处理key过时的时候,依赖gettimeofday方法得到时间,而不是monotonic clock,这也会带来时间的不许确。好比一下场景,两个客户端client 1和client 2,5个redis节点nodes (A, B, C, D and E)。

  • 一、client 1从A、B、C成功获取锁,从D、E获取锁网络超时。
  • 二、节点C的时钟不许确,致使锁超时。
  • 三、client 2从C、D、E成功获取锁,从A、B获取锁网络超时。
  • 四、这样client 1和client 2都得到了锁。

总结专家关于Redlock不可用的两点:

  • 一、GC等场景可能随时发生,并致使在客户端获取了锁,在处理中超时,致使另外的客户端获取了锁。专家还给出了使用自增token的解决方法。
  • 二、算法依赖本地时间,会出现时钟不许,致使2个客户端同时得到锁的状况。

因此专家给出的结论是,只有在有界的网络延迟、有界的程序中断、有界的时钟错误范围,Redlock才能正常工做,可是这三种场景的边界又是没法确认的,因此专家不建议使用Redlock。对于正确性要求高的场景,专家推荐了Zookeeper,关于使用Zookeeper做为分布式锁后面再讨论。

redis做者解疑Redlock

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解决锁超时问题能够归纳成下面五点:

  • 观点1,使用分布式锁通常是在,你没有其余方式去控制共享资源了,专家使用token来保证对共享资源的处理,那么就不须要分布式锁了。
  • 观点2,对于token的生成,为保证不一样客户端得到的token的可靠性,生成token的服务仍是须要分布式锁保证服务的可靠性。
  • 观点3,对于专家说的自增的token的方式,redis做者认为彻底不必,每一个客户端能够生成惟一的uuid做为token,给共享资源设置为只有该uuid的客户端才能处理的状态,这样其余客户端就没法处理该共享资源,直到得到锁的客户端释放锁。
  • 观点四、redis做者认为,对于token是有序的,并不能解决专家提出的GC问题,如上图所示,若是token 34的客户端写入过程当中发送GC致使锁超时,另外的客户端可能得到token 35的锁,并再次开始写入,致使锁冲突。因此token的有序并不能跟共享资源结合起来。
  • 观点五、redis做者认为,大部分场景下,分布式锁用来处理非事务场景下的更新问题。做者意思应该是有些场景很难结合token处理共享资源,因此得依赖锁去锁定资源并进行处理。

专家说到的另外一个时钟问题,redis做者也给出了解释。客户端实际得到的锁的时间是默认的超时时间,减去获取锁所花费的时间,若是获取锁花费时间过长致使超过了锁的默认超时间,那么此时客户端并不能获取到锁,不会存在专家提出的例子。

再次分析Redlock

看了两位专家你来我回的争辩,相信读者会对Redlock有了更多的认识。这里我也想就分布式专家提到的两个问题结合redis做者的观点,说说个人想法。

第一个问题我归纳为,在一个客户端获取了分布式锁后,在客户端的处理过程当中,可能出现锁超时释放的状况,这里说的处理中除了GC等非抗力外,程序流程未处理完也是可能发生的。以前在说到数据库锁设置的超时时间2分钟,若是出现某个任务占用某个订单锁超过2分钟,那么另外一个交易中心就能够得到这把订单锁,从而两个交易中心同时处理同一个订单。正常状况,任务固然秒级处理完成,但是有时候,加入某个rpc请求设置的超时时间过长,一个任务中有多个这样的超时请求,那么,极可能就出现超过自动解锁时间了。当初咱们的交易模块是用C++写的,不存在GC,若是用java写,中间还可能出现Full GC,那么锁超时解锁后,本身客户端没法感知,是件很是严重的事情。我以为这不是锁自己的问题,上面说到的任何一个分布式锁,只要自带了超时释放的特性,都会出现这样的问题。若是使用锁的超时功能,那么客户端必定得设置获取锁超时后,采起相应的处理,而不是继续处理共享资源。Redlock的算法,在客户端获取锁后,会返回客户端能占用的锁时间,客户端必须处理该时间,让任务在超过该时间后中止下来。

第二个问题,天然就是分布式专家没有理解Redlock。Redlock有个关键的特性是,获取锁的时间是锁默认超时的总时间减去获取锁所花费的时间,这样客户端处理的时间就是一个相对时间,就跟本地时间无关了。

由此看来,Redlock的正确性是能获得很好的保证的。仔细分析Redlock,相比于一个节点的redis,Redlock提供的最主要的特性是可靠性更高,这在有些场景下是很重要的特性。可是我以为Redlock为了实现可靠性,却花费了过大的代价。

  • 首先必须部署5个节点才能让Redlock的可靠性更强。
  • 而后须要请求5个节点才能获取到锁,经过Future的方式,先并发向5个节点请求,再一块儿得到响应结果,能缩短响应时间,不过仍是比单节点redis锁要耗费更多时间。
  • 而后因为必须获取到5个节点中的3个以上,因此可能出现获取锁冲突,即你们都得到了1-2把锁,结果谁也不能获取到锁,这个问题,redis做者借鉴了raft算法的精髓,经过冲突后在随机时间开始,能够大大下降冲突时间,可是这问题并不能很好的避免,特别是在第一次获取锁的时候,因此获取锁的时间成本增长了。
  • 若是5个节点有2个宕机,此时锁的可用性会极大下降,首先必须等待这两个宕机节点的结果超时才能返回,另外只有3个节点,客户端必须获取到这所有3个节点的锁才能拥有锁,难度也加大了。
  • 若是出现网络分区,那么可能出现客户端永远也没法获取锁的状况。

分析了这么多缘由,我以为Redlock的问题,最关键的一点在于Redlock须要客户端去保证写入的一致性,后端5个节点彻底独立,全部的客户端都得操做这5个节点。若是5个节点有一个leader,客户端只要从leader获取锁,其余节点能同步leader的数据,这样,分区、超时、冲突等问题都不会存在。因此为了保证分布式锁的正确性,我以为使用强一致性的分布式协调服务能更好的解决问题。

更好的分布式锁—zookeeper

提到分布式协调服务,天然就想到了zookeeper。zookeeper实现了相似paxos协议,是一个拥有多个节点分布式协调服务。对zookeeper写入请求会转发到leader,leader写入完成,并同步到其余节点,直到全部节点都写入完成,才返回客户端写入成功。

zookeeper还有几个特质,让它很是适合做为分布式锁服务。

  • zookeeper支持watcher机制,这样实现阻塞锁,能够watch锁数据,等到数据被删除,zookeeper会通知客户端去从新竞争锁。
  • zookeeper的数据能够支持临时节点的概念,即客户端写入的数据是临时数据,在客户端宕机后,临时数据会被删除,这样就实现了锁的异常释放。使用这样的方式,就不须要给锁增长超时自动释放的特性了。

zookeeper实现锁的方式是客户端一块儿竞争写某条数据,好比/path/lock,只有第一个客户端能写入成功,其余的客户端都会写入失败。写入成功的客户端就得到了锁,写入失败的客户端,注册watch事件,等待锁的释放,从而继续竞争该锁。

若是要实现tryLock,那么竞争失败就直接返回false便可。

zookeeper实现的分布式锁简单、明了,分布式锁的关键技术都由zookeeper负责实现了。能够看下《从Paxos到Zookeeper:分布式一致性原理与实践》书里贴出来的分布式锁实现步骤

Alt text

须要使用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不会那么闲置。

参考资料:

相关文章
相关标签/搜索