当前位置:   article > 正文

实现分布式锁框架分析

tair怎么保证原子性

640?wx_fmt=png

分布式锁实现方式

一、数据库锁

一般很少使用数据库锁,性能不好并且容易产生死锁

1. 基于MySQL锁表

该实现方式完全依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。这种方式存在以下几个问题:

(1) 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获取到锁,因为唯一索引insert都会返回失败。

(2) 只能是非阻塞锁,insert失败直接就报错了,无法进入队列进行重试

(3) 不可重入,同一线程在没有释放锁之前无法再获取到锁

2. 采用乐观锁增加版本号

根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。

二、缓存锁

具体实例可以参考我讲述Redis的系列文章,里面有完整的Redis分布式锁实现方案

这里我们主要介绍几种基于redis实现的分布式锁:

1. 基于setnx、expire两个命令来实现

基于setnx(set if not exist)的特点,当缓存里key不存在时,才会去set,否则直接返回false。如果返回true则获取到锁,否则获取锁失败,为了防止死锁,我们再用expire命令对这个key设置一个超时时间来避免。但是这里看似完美,实则有缺陷,当我们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。

解决上述问题有两种方案

第一种是采用redis2.6.12版本以后的set,它提供了一系列选项

  • EX seconds – 设置键key的过期时间,单位时秒

  • PX milliseconds – 设置键key的过期时间,单位时毫秒

  • NX – 只有键key不存在的时候才会设置key的值

  • XX – 只有键key存在的时候才会设置key的值

第二种采用setnx(),get(),getset()实现,大体的实现过程如下:

(1) 线程Asetnx,值为超时的时间戳(t1),如果返回true,获得锁。

(2) 线程B用get 命令获取t1,与当前时间戳比较,判断是否超时,没超时false,如果已超时执行步骤3

(3) 计算新的超时时间t2,使用getset命令返回t3(这个值可能其他线程已经修改过),如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了

(4) 获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)

2. RedLock算法

redlock算法是redis作者推荐的一种分布式锁实现方式,算法的内容如下:

(1) 获取当前时间;

(2) 尝试从5个相互独立redis客户端获取锁;

(3) 计算获取所有锁消耗的时间,当且仅当客户端从多数节点获取锁,并且获取锁的时间小于锁的有效时间,认为获得锁;

(4) 重新计算有效期时间,原有效时间减去获取锁消耗的时间;

(5) 删除所有实例的锁

redlock算法相对于单节点redis锁可靠性要更高,但是实现起来条件也较为苛刻。

(1) 必须部署5个节点才能让Redlock的可靠性更强。

(2) 需要请求5个节点才能获取到锁,通过Future的方式,先并发向5个节点请求,再一起获得响应结果,能缩短响应时间,不过还是比单节点redis锁要耗费更多时间。

然后由于必须获取到5个节点中的3个以上,所以可能出现获取锁冲突,即大家都获得了1-2把锁,结果谁也不能获取到锁,这个问题,redis作者借鉴了raft算法的精髓,通过冲突后在随机时间开始,可以大大降低冲突时间,但是这问题并不能很好的避免,特别是在第一次获取锁的时候,所以获取锁的时间成本增加了。

如果5个节点有2个宕机,此时锁的可用性会极大降低,首先必须等待这两个宕机节点的结果超时才能返回,另外只有3个节点,客户端必须获取到这全部3个节点的锁才能拥有锁,难度也加大了。

如果出现网络分区,那么可能出现客户端永远也无法获取锁的情况,介于这种情况,下面我们来看一种更可靠的分布式锁zookeeper锁。

zookeeper分布式锁

关于zookeeper的分布式锁实现在之前讲述zookeeper的时候已经介绍了。这里不再赘述、

首先我们来了解一下zookeeper的特性,看看它为什么适合做分布式锁,

zookeeper是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个唯一文件名。

数据模型:

  • 永久节点:节点创建后,不会因为会话失效而消失

  • 临时节点:与永久节点相反,如果客户端连接失效,则立即删除节点

  • 顺序节点:与上述两个节点特性类似,如果指定创建这类节点时,zk会自动在节点名后加一个数字后缀,并且是有序的。

监视器(watcher):

  • 当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。

根据zookeeper的这些特性,我们来看看如何利用这些特性来实现分布式锁:

1. 创建一个锁目录lock

2. 希望获得锁的线程A就在lock目录下,创建临时顺序节点

3. 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁

4. 线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)

5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁。

小结

在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常用的解决方案就是使用分布式锁,这里只介绍了几种常用的分布式锁,分布式锁的实现方式还有有很多种,根据业务选择合适的分布式锁,下面对上述几种锁进行一下比较:

数据库锁:

  • 优点:直接使用数据库,使用简单。

  • 缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。

缓存锁:

  • 优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。

  • 缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。

zookeeper锁:

  • 优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。

  • 缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。


锁的演变

单网络节点

    基于不同层级我们实现锁的机制:

        1.基于数据库

            (1)基于sql的for update

            (2)基于表字段(一般利用增加字段version版本号来控制)

        2.基于java语法      

            (1)synchronized

        (2)java.util.concurrent.locks包下常用的类

多网络节点

        1.基于数据库实现分布式锁

        2.基于缓存(redis,memcached,tair)实现分布式锁

        3.基于Zookeeper实现分布式锁


常用分布式锁方案的实现:

单机

synchronized、juc

分布式锁

  1. 互斥性:在任意时刻,只有一个客户端能持有锁

  2. 不会发生死锁:即有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁

常见方案

  1. 基于数据库

  2. 基于分布式缓存(redis、tair等)

  3. 基于zk 要基于你的业务场景选择合适方案

三大分布式框架分析:

zookeeper引擎分析

优点:

  • 锁安全性高,zk可持久化,且能实时监听获取锁的客户端状态。

  • zookeeper支持watcher机制,这样实现阻塞锁,可以watch锁数据,等到数据被删除,zookeeper会通知客户端去重新竞争锁。

  • zookeeper的数据可以支持临时节点的概念,即客户端写入的数据是临时数据,在客户端宕机后,临时数据会被删除,这样就实现了锁的异常释放。使用这样的方式,就不需要给锁增加超时自动释放的特性了。

缺点:

  • 性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。所以不太适合直接提供给高并发的场景使用。

  • zk使用的Zab的一致性协议,写是一个两阶段协议,效率上要差一些。

  • 使用watch时,由于watch使用较多,watch对zookeeper性能有一定影响。

适用场景:

  • 对可靠性要求非常高,且并发程度不高的场景下使用。如核心数据的定时全量/增量同步等。

tair引擎分析

优点:

  • 同时支持分布式缓存和持久化存储。

  • 自动的复制和迁移,自动扩容。

  • tair支持Version、原子计数、和Item支持。

  • 使用的中心化的架构设计和一致性 hash 算法的数据分布,同时支持在线数据迁移。

  • 数据可靠性⾼、成本低。

缺点:

  • 在⼤并发访问下性能可能会有较⼤抖动

  • 在某些情况下(如客户端gc、磁盘io、慢请求阻塞)会导致请求超时问题,在分布式锁的使用中会导致获取锁失败。

场景:

  • 数据规模较⼤、冷热数据显著的业务场景,对分布式锁可靠性有一定要求,但并发量要求没有太高的时候使用。

redis引擎分析

优点:

  • 并发高效,redis自3.0自身支持集群,同时支持哨兵机制,高性能低延迟。

  • redis可以持久化数据。

  • redis使用单进程单线程,内存存储,速度非常快,比memcached还要快很多,所以支持的并发访问量可以很高。

  • 现已有成熟的java客户端,如jedis。

缺点:

  • redis采用某些淘汰策略,所以如果内存不够,可能导致缓存中的锁信息丢失。

  • 一旦缓存服务宕机,锁数据就丢失了。像redis自带复制功能,可以对数据可靠性有一定的保证,但是由于复制也是异步完成的,因此依然可能出现master节点写入锁数据而未同步到slave节点的时候宕机,锁数据丢失问题。

  • 需要加入超时机制避免死锁。

  • 成本较高。

场景:

  • 高并发,需要加入超时机制避免死锁,提供足够的支撑锁服务的内存空间,稳定的集群化管理,同时没有对数据的可靠性有较高要求。

自研分布式锁分析

优点:

  • 提供了引擎的多种选择,多种可靠性和不同并发量的阶梯选择。

  • 可扩展性强,可以加入其他引擎。

  • zk引擎和tair引擎目前都支持可重入读写锁。

  • 作为一个sdk,可以使用jar包直接导入,业务使用简单。

缺点:

  • 目前相对而言,相对粗糙,多种功能未完成,已有功能需完善。

  • 目前没有管理界面和工具,排错需要到集群上和业务机器上进行。

  • 没有建立容灾机制。

  • tair请求超时问题未解决。

  • tair的可重入读写锁暂时支持的不够好,需要研究改进。

  • redis存在redlock的问题:锁失效问题和单点问题。

  • 没有提供引擎的降级方案,也不能一键切换引擎,需要业务机器停下业务逐个切换。

  • 需要提供专属集群。

  • 代码层次需要优化,注释相对较少。

项目的改进

  1. curator最流行的zookeeper的客户端,对分布式锁有很好的支持,有提供模仿jdk锁的API,对项目的后期改进空间较大,故zookeeper引擎内部zookeeper客户端换成curator。

  2. 增加一个服务端以及web界面,管理分布式锁客户端机器的状态信息(记录连接机器的IP地址、持有锁的机器的IP地址、机器的appkey等),以及集群的锁的记录等信息管理和查看。

  3. 由于业务在运行时对引擎进行切换,服务端会丢失锁的记录信息,而且没有较好的解决方案;同时大多数业务在给出需求时就可以确定最合适的引擎,故除去引擎降级方案,增加另一集群进行切换。

  4. 需要建立容灾机制,双中心容灾或异地容灾后期研究。

  5. 目前已知tair引擎在并发量大的时候会出现请求超时问题,需要查看集群状态,后期研究改进。

  6. 提供鉴权,同时对appkey和secret的校验移至服务端进行。

  7. 提供统计功能:统计单个机器调用分布式锁次数,调用成功和失败次数,异常统计。

  8. 解决redlock的问题,同时squirrel的redission的方案进行研究。

一般我们使用分布式锁有两个场景:

  • 效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。

  • 正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。

分布式锁的一些特点

当我们确定了在不同节点上需要分布式锁,那么我们需要了解分布式锁到底应该有哪些特点?

分布式锁的特点如下:

  • 互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。

  • 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。

  • 锁超时:和本地锁一样支持锁超时,防止死锁。

  • 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。

  • 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。

  • 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。


常见的分布式锁

我们了解了一些特点之后,我们一般实现分布式锁有以下几个方式:

  • MySQL

  • ZK

  • Redis

  • 自研分布式锁:如谷歌的 Chubby。

下面分开介绍一下这些分布式锁的实现原理。

MySQL

首先来说一下 MySQL 分布式锁的实现原理,相对来说这个比较容易理解,毕竟数据库和我们开发人员在平时的开发中息息相关。

对于分布式锁我们可以创建一个锁表:

640?wx_fmt=jpeg

前面我们所说的 lock(),trylock(long timeout),trylock() 这几个方法可以用下面的伪代码实现。

lock()

lock 一般是阻塞式的获取锁,意思就是不获取到锁誓不罢休,那么我们可以写一个死循环来执行其操作:

640?wx_fmt=png

mysqlLock.lcok 内部是一个 sql,为了达到可重入锁的效果,我们应该先进行查询,如果有值,需要比较 node_info 是否一致。

这里的 node_info 可以用机器 IP 和线程名字来表示,如果一致就加可重入锁 count 的值,如果不一致就返回 false。如果没有值就直接插入一条数据。

需要注意的是这一段代码需要加事务,必须要保证这一系列操作的原子性。

tryLock() 和 tryLock(long timeout)

tryLock() 是非阻塞获取锁,如果获取不到就会马上返回,代码如下:

640?wx_fmt=png

tryLock(long timeout) 实现如下:

640?wx_fmt=jpeg

mysqlLock.lock 和上面一样,但是要注意的是 select … for update 这个是阻塞的获取行锁,如果同一个资源并发量较大还是有可能会退化成阻塞的获取锁。

unlock()

unlock 的话如果这里的 count 为 1 那么可以删除,如果大于 1 那么需要减去 1。

锁超时

我们有可能会遇到我们的机器节点挂了,那么这个锁就不会得到释放,我们可以启动一个定时任务,通过计算一般我们处理任务的时间。

比如是 5ms,那么我们可以稍微扩大一点,当这个锁超过 20ms 没有被释放我们就可以认定是节点挂了然后将其直接释放。

MySQL 小结:

  • 适用场景:MySQL 分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤。比如一个订单,我们可以用 select * from order_table where id = 'xxx' for update 进行加行锁,那么其他的事务就不能对其进行修改。

  • 优点:理解起来简单,不需要维护额外的第三方中间件(比如 Redis,ZK)。

  • 缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。

乐观锁

前面我们介绍的都是悲观锁,这里想额外提一下乐观锁,在我们实际项目中也是经常实现乐观锁,因为我们加行锁的性能消耗比较大,通常我们对于一些竞争不是那么激烈。

但是其又需要保证我们并发的顺序执行使用乐观锁进行处理,我们可以对我们的表加一个版本号字段。

那么我们查询出来一个版本号之后,update 或者 delete 的时候需要依赖我们查询出来的版本号,判断当前数据库和查询出来的版本号是否相等,如果相等那么就可以执行,如果不等那么就不能执行。

这样的一个策略很像我们的 CAS(Compare And Swap),比较并交换是一个原子操作。这样我们就能避免加 select * for update 行锁的开销。

ZooKeeper

ZooKeeper 也是我们常见的实现分布式锁方法,相比于数据库如果没了解过 ZooKeeper 可能上手比较难一些。

ZooKeeper 是以 Paxos 算法为基础的分布式应用程序协调服务。ZK 的数据节点和文件目录类似,所以我们可以用此特性实现分布式锁。

我们以某个资源为目录,然后这个目录下面的节点就是我们需要获取锁的客户端,未获取到锁的客户端注册需要注册 Watcher 到上一个客户端,可以用下图表示:

640?wx_fmt=jpeg

/lock 是我们用于加锁的目录,/resource_name 是我们锁定的资源,其下面的节点按照我们加锁的顺序排列。

Curator

Curator 封装了 ZooKeeper 底层的 API,使我们更加容易方便的对 ZooKeeper 进行操作,并且它封装了分布式锁的功能,这样我们就不需要在自己实现了。

Curator 实现了可重入锁(InterProcessMutex),也实现了不可重入锁(InterProcessSemaphoreMutex)。在可重入锁中还实现了读写锁。

InterProcessMutex

InterProcessMutex 是 Curator 实现的可重入锁,我们可以通过下面的一段代码实现我们的可重入锁:

640?wx_fmt=png

我们利用 acuire 进行加锁,release 进行解锁。

锁超时

ZooKeeper 不需要配置锁超时,由于我们设置节点是临时节点,我们的每个机器维护着一个 ZK 的 Session,通过这个 Session,ZK 可以判断机器是否宕机。

如果我们的机器挂掉的话,那么这个临时节点对应的就会被删除,所以我们不需要关心锁超时。

ZK 小结:

  • 优点:ZK 可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK 获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用 ZK 集群进行保证。

  • 缺点:ZK 需要额外维护,增加维护成本,性能和 MySQL 相差不大,依然比较差。并且需要开发人员了解 ZK 是什么。

Redis

大家在网上搜索分布式锁,恐怕最多的实现就是 Redis 了,Redis 因为其性能好,实现起来简单所以让很多人都对其十分青睐。

Redis 分布式锁简单实现

熟悉 Redis 的同学那么肯定对 setNx(set if not exist) 方法不陌生,如果不存在则更新,其可以很好的用来实现我们的分布式锁。

对于某个资源加锁我们只需要:

setNx resourceName value

这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和 setNx 同一个原子操作。

在 Redis 2.8 之前我们需要使用 Lua 脚本达到我们的目的,但是 Redis 2.8 之后 Redis 支持 nx 和 ex 操作是同一原子操作。

setresourceName valueex 5nx

RedLock 基本原理是利用多个 Redis 集群,用多数的集群加锁成功,减少 Redis 某个集群出故障,造成分布式锁出现问题的概率。

Redis 小结:

  • 优点:对于 Redis 实现简单,性能对比 ZK 和 MySQL 较好。如果不需要特别复杂的要求,自己就可以利用 setNx 进行实现,如果自己需要复杂的需求的话,可以利用或者借鉴 Redission。对于一些要求比较严格的场景可以使用 RedLock。

  • 缺点:需要维护 Redis 集群,如果要实现 RedLock 需要维护更多的集群。

分布式锁的安全问题

前面说的算法基本都有安全问题,下面我们来讨论一下这些问题。

长时间的 GC pause

熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 STW(stop-the-world)。

例如 CMS 垃圾回收器,它会有两个阶段进行 STW 防止引用继续进行变化。那么有可能会出现下面图(引用至 Martin 反驳 Redlock 的文章)中这个情况:

640?wx_fmt=png

client1 获取了锁并且设置了锁的超时时间,但是 client1 之后出现了 STW,这个 STW 时间比较长,导致分布式锁进行了释放。

client2 获取到了锁,这个时候 client1 恢复了锁,那么就会出现 client1,2 同时获取到锁,这个时候分布式锁不安全问题就出现了。

这个不仅仅局限于 RedLock,对于我们的 ZK,MySQL 一样的有同样的问题。

时钟发生跳跃

对于 Redis 服务器如果其时间发生了跳跃,肯定会影响我们锁的过期时间。

那么我们的锁过期时间就不是我们预期的了,也会出现 client1 和 client2 获取到同一把锁,也会出现不安全,这个对于 MySQL 也会出现。但是 ZK 由于没有设置过期时间,那么发生跳跃也不会受影响。

长时间的网络 I/O

这个问题和我们的 GC 的 STW 很像,也就是我们这个获取了锁之后我们进行网络调用,其调用时间由可能比我们锁的过期时间都还长,那么也会出现不安全的问题,这个 MySQL 也会有,ZK 也不会出现这个问题。

对于这三个问题,在网上包括 Redis 作者在内发起了很多讨论。

GC 的 STW

对于这个问题可以看见基本所有的都会出现问题,Martin 给出了一个解法,对于 ZK 这种他会生成一个自增的序列,那么我们真正进行对资源操作的时候,需要判断当前序列是否是最新,有点类似于乐观锁。

当然这个解法 Redis 作者进行了反驳,你既然都能生成一个自增的序列了那么你完全不需要加锁了,也就是可以按照类似于 MySQL 乐观锁的解法去做。

我自己认为这种解法增加了复杂性,当我们对资源操作的时候需要增加判断序列号是否是最新,无论用什么判断方法都会增加复杂度,后面会介绍谷歌的 Chubby 提出了一个更好的方案。

时钟发生跳跃

Martin 觉得 RedLock 不安全很大的原因也是因为时钟的跳跃,因为锁过期强依赖于时间,但是 ZK 不需要依赖时间,依赖每个节点的 Session。

Redis 作者也给出了解答,对于时间跳跃分为人为调整和 NTP 自动调整:

  • 人为调整:人为调整影响的完全可以人为不调整,这个是处于可控的。

  • NTP 自动调整:这个可以通过一定的优化,把跳跃时间控制在可控范围内,虽然会跳跃,但是是完全可以接受的。

长时间的网络 I/O

这一块不是他们讨论的重点,我自己觉得,对于这个问题的优化可以控制网络调用的超时时间,把所有网络调用的超时时间相加。

那么我们锁过期时间其实应该大于这个时间,当然也可以通过优化网络调用比如串行改成并行,异步化等。

Chubby 的一些优化

大家搜索 ZK 的时候,会发现他们都写了 ZK 是 Chubby 的开源实现,Chubby 内部工作原理和 ZK 类似。但是 Chubby 的定位是分布式锁和 ZK 有点不同。

Chubby 也是使用上面自增序列的方案用来解决分布式不安全的问题,但是它提供了多种校验方法:

  • CheckSequencer():调用 Chubby 的 API 检查此时这个序列号是否有效。

  • 访问资源服务器检查,判断当前资源服务器最新的序列号和我们的序列号的大小。

  • lock-delay:为了防止我们校验的逻辑入侵我们的资源服务器,其提供了一种方法当客户端失联的时候,并不会立即释放锁,而是在一定的时间内(默认 1min)阻止其他客户端拿去这个锁。那么也就是给予了一定的 buffer 等待 STW 恢复,而我们的 GC 的 STW 时间如果比 1min 还长那么你应该检查你的程序,而不是怀疑你的分布式锁了。

本文主要讲了多种分布式锁的实现方法,以及它们的一些优缺点。最后也说了一下关于分布式锁的安全的问题。

对于不同的业务需要的安全程度完全不同,我们需要根据自己的业务场景,通过不同的维度分析,选取最适合自己的方案。


基于zookeeper,其实可以设计出优雅的分布式锁。这个锁可以有这几个API:lock()unlock()isLock(),lock用于加锁,unlock用于解锁,isLock用于判断是否已经锁住了。zookeeper提供了这么一套机制,你可以监控watch节点的变化(内容更新,子节点添加,删除),然后节点变化的时候通过回调我们的监控器(watcher)来通知我们节点的实时变化。在这种机制下,我们可以很简单的做一个锁。

在单机模式,没有引入zookeeper时,我们可以通过创建一个临时文件来加锁,然后在事务处理完毕的时候,把这个临时文件删除就能代表解锁了。这种简单的加锁和解锁模式可以移植到zookeeper上,通过创建一个路径,来证明该锁已经存在,然后删除路径来释放该锁。而同时zookeeper又能支持对节点的监控,这样一来,我们在多机的情况下就能同时且实时知道锁是存在还是已经解锁了。如图所示,我们在/lock下创建了/app1  /app2 … /appN n个子目录,用于适用不同的应用, 每个/app* 下面都可以根据业务需求创建锁 /lock. , 而每个机器在获取锁的时候,会在/lock. 下面创建 _0000000* 的自增长临时节点,这个节点上的数字用于表示获取锁的先后顺序。前面说的还是有点抽象,下面举个例子:

一个后台应用(app为back)总共有3台机器在处理事务,其中有一个业务(lock为 biz)同一时间只能有一台机器能处理,其他如果也同时收到处理消息的时候,需要对这个事务加个锁,以保证全局的事务性。三台机器分别表示为 server1, server2和 server3。

640?wx_fmt=png

对应的,锁的路径就是 /lock/back/biz 。 首先 server1先收到消息,要处理事务biz,那么获取锁,并在/lock/back/biz下创建一个临时节点/lock/back/biz/_00000001 ,这时候判断/lock/back/biz 下的子节点,看最小的那个是不是和自己创建的相等,如果是,说明自己已经获取了锁,可以继续下一步的操作,直到事务完成。

640?wx_fmt=png

与此同时,server2和server3也收到的消息(稍微慢于server1收到),这时候server2和server3也分别会在/lock/back/biz下面创建临时节点/lock/back/biz/_00000002 和 /lock/back/biz/_00000003,他们这时候也会判断/lock/back/biz下的子节点,发现最小的节点是_00000001,不是自己创建的那个,于是乎就会进入一个wait的操作(通过mutex很容易实现)。之后,等server1的事务处理完毕,释放server1获取的锁,也就是把 /_00000001删掉。由于zookeeper可以监控节点的变化,删掉/_00000001的时候,zookeeper可以通过节点删除的事件,通知到server1 server2 server3,这时候server2和server3对上面通过mutex block住的区块发送信号量(notify),server2和server3继续进入判断/lock/back/biz下面的子节点以及最小的节点和自己做对比,这时候server2由于创建了节点_00000002,所以轮到他来获取锁:

640?wx_fmt=png

之后server2开始进入事务处理,然后释放锁,server3开始得到锁,处理事务,释放锁。以此类推。

这样一来,整个事务(biz)的处理就能保证同时只有一台机能处理到了。

整体伪代码如下:

  1. public class LockImpl{
  2. //获取zk实例,最好是单例的或者是共享连接池,不然并发高的时候,容易挂
  3. private Zookeeper zk = getZookeeper();
  4. //用于本地做wait和notify
  5. private byte[] mutex = new byte[0];
  6. //节点监控器
  7. private Watcher watcher;
  8. //这个锁生成的序列号
  9. private String serial;
  10. public LockImpl(){
  11. watcher = new DefaultWatcher(mutex);
  12. createIfNotExist();
  13. }
  14. private createIfNotExist(){
  15. String path = buildPath();
  16. //创建路径,如果不存在的话
  17. createIfNotExsitPath(path);
  18. //注册监控器,才能监控到。
  19. registWatcher(watcher);
  20. }
  21. public void lock(){
  22. //获取序列号,其实就是创建一个当前path下的临时节点,zk会返回该节点的值
  23. String serial = genSerial();
  24. while(true){
  25. //获取子节点
  26. List<string> children = getChildren();
  27. //按从小到大排序
  28. sort(children);
  29. //如果当前节点是第一个,说明被当前进程的线程获取。
  30. if(children.index(serial) == 0){
  31. //you get the lock
  32. break;
  33. }else{
  34. //否则等待别人删除节点后通知,然后进入下一次循环。
  35. synchronized(mutex){
  36. mutex.wait();
  37. }
  38. }
  39. }
  40. return;
  41. }
  42. public void unlock(){
  43. //删除子节点就能解锁
  44. deletePath(serial);
  45. }
  46. public void isLock(){
  47. //判断路径下面是否有子节点即可
  48. return ifHasChildren();
  49. }}//监控器类public class DefaultWatcher implements Watcher{
  50. private byte[] mutex;
  51. public DefaultWatcher(byte[] mutex){
  52. this.mutex = mutex;
  53. }
  54. public void process(WatchedEvent event){
  55. synchronized (mutex) {
  56. mutex.notifyAll();
  57. }
  58. }}

至此,一个看起来优雅一点的分布式锁就实现了。这是一个理想状态下的实现,咋看起来没问题,其实里面隐藏了比较严重的问题。就是这里把zookeeper理想化了,认为他是完美的,不会出现问题。这里说的问题倒不是一定是zk的bug,比如网络问题,在分布式系统中,网络问题是一个很常见的问题,很容易就会有异常的情况。如果出现网络问题,会出现什么情况呢?答案是“死锁”

下面按多种情况来分析这个问题:

当我们向zk发起请求,要求创建一个临时节点的时候,发生了异常(包括网络异常,zk的session失效等)

  1. 假设zk的服务端还没收到请求,这时候很简单,我们客户端做一下重连和重新创建就可以了,问题不大

  2. 假设zk服务端收到请求了,但服务端发生异常,没有创建成功,那么我们客户端再次重试一下就可以了。640?wx_fmt=png

  3. 假设zk服务端收到了请求,子节点创建成功了,但是返回给客户端的时候网络发生了异常。这时候我们要是再做重试,创建的话,就会进入“死锁”。这里的“死锁”跟我们平时理解的死锁不是一个概念,这里的意思不是回路循环等待,而是相当于这个锁就是死掉了,因为前面的异常请求中其实创建了一个节点,然后这个节点没有客户端与他关联,我们可以称为幽灵节点。这个幽灵节点由于先后顺序,他是优先级最高的,然而没有客户端跟他关联,也就是没有客户端可以释放这个幽灵节点,那么所有的客户端都会进入无限的等待,造成了“死锁”的现象。640?wx_fmt=png640?wx_fmt=png

  4. 假设zk服务端收到请求了,子节点创建成功了,返回给客户端成功了,但是客户端在处理的时发生了异常(这种一般是zk的bug才会出现),这时候我们再做一次重试,也会进入上面的“死锁”现象。640?wx_fmt=png为什么会出现3,4的现象呢?因为我们只是做了简单的重试,并没有对服务端和客户端做验证。就是客户端创建了一个幽灵节点,但是创建者本身甚至都不知道是自己创建的幽灵节点,还是别人创建的。要如何规避这个问题呢?废话,引入验证的流程。就是验证+无限重试。怎么做验证,不要把验证想的太复杂,验证就是你创建的节点里面有你创建的私有的信息,你客户端本身也拥有这个信息,然后两者一对比,就能知道哪个节点是哪个客户端创建的。当然,这个信息必须保证我有你无的,也就是唯一性。简单了,引入UUID就可以搞定这个问题。

1. 创建临时节点之前,客户端先生成一个UUID,(直接用JDK的UUID.randomUUID().toString()就可以了)。
2. 创建临时节点时,把这个uuid作为节点的一部分,创建出一个临时节点。
3. 重试创建的流程中加入对已存在的UUID的判断,对于是当前进程创建的子节点不再重复创建。
4. 对children排序的时候,把uuid去除后再排序,就能达到先进先出的效果。

比如客户端1,生成了UUID: fd456c28-cc85-4e2f-8d52-bcf7538a2acf, 然后创建了一个临时节点: /lock/back/biz/_fd456c28-cc85-4e2f-8d52-bcf7538a2acf_00000001

对children排序不能简单的把node的路径进行排序,因为randomUUID是完全随机的,按这个排序可能会导致某些锁请求一直没有被响应,也会有问题。这里因为UUID的长度是固定的,而且也有规律可循,所以很容易从node中分解出 uuid和序列号,然后对序列号进行排序,找出最小的值,再赋予锁,就可以了。

分布式系统中,异常出现很正常,如果你的业务需要幂等操作(N^m = N)的话,就需要引入验证和重试的机制。分布式锁就是需要一个觅等的操作,所以一个靠谱的分布式锁的实现,验证和重试的机制是少不了的,这就是我想说的。具体要参考实现的话,可以看netflix开源的zk客户端框架curator2, 比直接用zk简单得多也健壮得多。框架里面也实现了一套分布式锁(还有其他各种有用的东西),生产线其实可以直接拿来使用的,非常方便。

原文:http://ju.outofmemory.cn/entry/100977

如何使用分布式锁?

导读:通过一个很常见的业务场景,引出一个分布式锁的具体方案,如何使用分布式锁呢?通过本文了解下。

为什么用分布式锁?


在讨论这个问题之前,我们先来看一个业务场景:

系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

由于系统有一定的并发,所以会预先将商品的库存保存在redis中,用户下单的时候会更新redis的库存。

此时系统架构如下:

640?wx_fmt=png

但是这样一来会产生一个问题:假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。

而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。

这样的结果,是导致卖出了2个商品,然而其实库存只有1个。

很明显不对啊!这就是典型的库存超卖问题

我们很容易想到解决方案:用锁把2、3、4步锁住,让他们执行完之后,另一个线程才能进来执行第2步。

640?wx_fmt=png

按照上面的图,在执行第2步时,使用Java提供的synchronized或者ReentrantLock来锁住,然后在第4步执行完之后才释放锁。

这样一来,2、3、4 这3个步骤就被“锁”住了,多个线程之间只能串行化执行。

但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:

640?wx_fmt=png

增加机器之后,系统变成上图所示,我的天!

假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。

为什么呢?因为上图中的两个A系统,运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。

因此,这里的问题是:Java提供的原生锁机制在多机部署场景下失效了。

这是因为两台机器加的锁不是同一个锁(两个锁在不同的JVM里面)。

那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?

此时,就该分布式锁隆重登场了,分布式锁的思路是:

在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。

文字描述不太直观,我们来看下图:

640?wx_fmt=jpeg

通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用Java原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。

那么,如何实现分布式锁呢?接着往下看!

基于Redis实现分布式锁


上面分析为啥要使用分布式锁了,这里我们来具体看看分布式锁落地的时候应该怎么样处理。

最常见的一种方案就是使用Redis做分布式锁

使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。

具体代码是这样的:

  1. // 获取锁
  2. // NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
  3. SET anyLock unique_value NX PX
  4. 30000
  5. // 释放锁:通过执行一段lua脚本
  6. // 释放锁涉及到两条指令,这两条指令不是原子性的
  7. // 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
  8. if redis.call( "get" ,KEYS[1]) == ARGV[1] then
  9. return redis.call("del",KEYS[1])
  10. else
  11. return 0
  12. end

这种方式有几大要点:

  • 一定要用SET key value NX PX milliseconds 命令

  • value要具有唯一性

    这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。

    这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。

640?wx_fmt=png

除了要考虑客户端要怎么实现分布式锁之外,还需要考虑redis的部署问题。

redis有3种部署方式:

  • 单机模式

  • master-slave + sentinel选举模式

  • redis cluster模式

使用redis做分布式锁的缺点在于:如果采用单机部署模式,会存在单点问题,只要redis故障了。加锁就不行了。

采用master-slave模式,加锁的时候只对一个节点加锁,即便通过sentinel做了高可用,但是如果master节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

基于以上的考虑,其实redis的作者也考虑到这个问题,他提出了一个RedLock的算法,这个算法的意思大概是这样的:

假设redis的部署模式是redis cluster,总共有5个master节点,通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒

  • 轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒

  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)

  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了

  • 要是锁建立失败了,那么就依次删除这个锁

  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

640?wx_fmt=png

另一种方式:Redisson

Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?

回想一下上面说的,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。

SET anyLock unique_value NX PX 30000

这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。

这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~

我们来看看redisson是怎么实现的?先感受一下使用redission的爽:

  1. Config config = new Config();
  2. config.useClusterServers()
  3. .addNodeAddress("redis://192.168.31.101:7001")
  4. .addNodeAddress("redis://192.168.31.101:7002")
  5. .addNodeAddress("redis://192.168.31.101:7003")
  6. .addNodeAddress("redis://192.168.31.102:7001")
  7. .addNodeAddress("redis://192.168.31.102:7002")
  8. .addNodeAddress("redis://192.168.31.102:7003");
  9. RedissonClient redisson = Redisson.create(config);
  10. RLock lock = redisson.getLock("anyLock");
  11. lock.lock();
  12. lock.unlock();

就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:

  • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行

  • redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?

    redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s

    这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。

  • redisson的“看门狗”逻辑保证了没有死锁发生。

(如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)

640?wx_fmt=png

这里稍微贴出来其实现代码:

  1. // 加锁逻辑
  2. private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
  3. if (leaseTime != -1) {
  4. return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
  5. }
  6. // 调用一段lua脚本,设置一些key、过期时间
  7. RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  8. ttlRemainingFuture.addListener(new FutureListener<Long>() {
  9. @Override
  10. public void operationComplete(Future<Long> future) throws Exception {
  11. if (!future.isSuccess()) {
  12. return;
  13. }
  14. Long ttlRemaining = future.getNow();
  15. // lock acquired
  16. if (ttlRemaining == null) {
  17. // 看门狗逻辑
  18. scheduleExpirationRenewal(threadId);
  19. }
  20. }
  21. });
  22. return ttlRemainingFuture;
  23. }
  24. <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
  25. internalLockLeaseTime = unit.toMillis(leaseTime);
  26. return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
  27. "if (redis.call('exists', KEYS[1]) == 0) then " +
  28. "redis.call('hset', KEYS[1], ARGV[2], 1); " +
  29. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  30. "return nil; " +
  31. "end; " +
  32. "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
  33. "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
  34. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  35. "return nil; " +
  36. "end; " +
  37. "return redis.call('pttl', KEYS[1]);",
  38. Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
  39. }
  40. // 看门狗最终会调用了这里
  41. private void scheduleExpirationRenewal(final long threadId) {
  42. if (expirationRenewalMap.containsKey(getEntryName())) {
  43. return;
  44. }
  45. // 这个任务会延迟10s执行
  46. Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  47. @Override
  48. public void run(Timeout timeout) throws Exception {
  49. // 这个操作会将key的过期时间重新设置为30s
  50. RFuture<Boolean> future = renewExpirationAsync(threadId);
  51. future.addListener(new FutureListener<Boolean>() {
  52. @Override
  53. public void operationComplete(Future<Boolean> future)

另外,redisson还提供了对redlock算法的支持,

  1. RedissonClient redisson = Redisson.create(config);
  2. RLock lock1 = redisson.getFairLock("lock1");
  3. RLock lock2 = redisson.getFairLock("lock2");
  4. RLock lock3 = redisson.getFairLock("lock3");
  5. RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
  6. multiLock.lock();
  7. multiLock.unlock();

小结:

本节分析了使用redis作为分布式锁的具体落地方案

以及其一些局限性

然后介绍了一个redis的客户端框架redisson,

这也是我推荐大家使用的,

比自己写代码实现会少care很多细节。

基于zookeeper实现分布式锁

常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。

在介绍zookeeper(下文用zk代替)实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:

Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。

zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:

  • 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;

  • zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号。也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。

  • 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。

  • 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:

  • 节点创建

  • 节点删除

  • 节点数据修改

  • 子节点变更

基于以上的一些zk的特性,我们很容易得出使用zk实现分布式锁的落地方案:

  1. 使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。

  2. 创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点

  3. 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。

  4. 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。

比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。

如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。

比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。

整个过程如下:

640?wx_fmt=jpeg

具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。

Curator介绍


Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。

他的使用方式也比较简单:

InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");

interProcessMutex.acquire();

interProcessMutex.release();

其实现分布式锁的核心源码如下:

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception

{

boolean haveTheLock = false;

boolean doDelete = false;

try {

if ( revocable.get() != null ) {

client.getData().usingWatcher(revocableWatcher).forPath(ourPath);

}


while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {

// 获取当前所有节点排序后的集合

List<String> children = getSortedChildren();

// 获取当前节点的名称

String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash

// 判断当前节点是否是最小的节点

PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);

if ( predicateResults.getsTheLock() ) {

// 获取到锁

haveTheLock = true;

} else {

// 没获取到锁,对当前节点的上一个节点注册一个监听器

String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();

synchronized(this){

Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);

if ( stat != null ){

if ( millisToWait != null ){

millisToWait -= (System.currentTimeMillis() - startMillis);

startMillis = System.currentTimeMillis();

if ( millisToWait <= 0 ){

doDelete = true; // timed out - delete our node

break;

}

wait(millisToWait);

}else{

wait();

}

}

}

// else it may have been deleted (i.e. lock released). Try to acquire again

}

}

}

catch ( Exception e ) {

doDelete = true;

throw e;

} finally{

if ( doDelete ){

deleteOurPath(ourPath);

}

}

return haveTheLock;

}

其实curator实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:

640?wx_fmt=jpeg

小结


本节介绍了zookeeperr实现分布式锁的方案以及zk的开源客户端的基本使用,简要的介绍了其实现原理。

两种方案的优缺点比较

学完了两种分布式锁的实现方案之后,本节需要讨论的是redis和zk的实现方案中各自的优缺点。

对于redis的分布式锁而言,它有以下缺点:

  • 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。

  • 另外来说的话,redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮

  • 即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题,关于redlock的讨论可以看How to do distributed locking

  • redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”

所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。

对于zk分布式锁而言:

  • zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。

  • 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

但是zk也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

综上所述,redis和zookeeper都有其优缺点。我们在做技术选型的时候可以根据这些问题作为参考因素。

作者的一些建议


通过前面的分析,实现分布式锁的两种常见方案:redis和zookeeper,他们各有千秋。应该如何选型呢?

就个人而言的话,我比较推崇zk实现的锁:

因为redis是有可能存在隐患的,可能会导致数据不对的情况。但是,怎么选用要看具体在公司的场景了。

如果公司里面有zk集群条件,优先选用zk实现,但是如果说公司里面只有redis集群,没有条件搭建zk集群。

那么其实用redis来实现也可以,另外还可能是系统设计者考虑到了系统已经有redis,但是又不希望再次引入一些外部依赖的情况下,可以选用redis。

这个是要系统设计者基于架构的考虑了。

原文:https://www.enmotech.com/web/detail/1/768/1.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/710680
推荐阅读
相关标签
  

闽ICP备14008679号