首页 > distributed

 

为了解决在分布式系统中需要对某个资源进行全局的一个非重复ID生成,所以有了分布式ID这么一个概念

 

在分布式应用下,像分库分表的这种场景是很常见的, 这个时候如果还是用数据库本身的自增的话,那多个数据库ID肯定会重复。

比如订单表由于数据过多,分到了多个数据库中存储的话,那么这个ID要如何生成呢?

还是以原来的逻辑进行自增的话,那就会出现这种情况:   数据库A里边有订单1、2、3 , 数据库B里边也有个订单1、2、3, 这在业务逻辑上就冲突了。

 

 

那么这就需要有一个分布式ID生成系统

而一个分布式ID系统需要满足的有这么几点:

1、高可用, 所有的业务系统都需要调用这么一种id生成器来生成id,这玩意千万不能挂

2、趋势有序, 无论是啥关系型数据库,在一个字段上加索引肯定是有序的更快

 

 

解决方案:

 

1、UUID

这个建议不用管,虽然好处多(无脑)。但是代价太大了。

 

主要就是在DB里边用UUID太长了,空间又占得大,你建的索引越多, 影响越严重。

UUID还是非自增,无论是增删改查相对于自增id的比较大小来说都要慢很多,尤其是在那种取数据段的场景。

 

 

2、数据库ID自增

那为了保证高可用,一个DB肯定是不行的,那必须得集群才行。

就用mysql举例:

主从:  那肯定是不行的。 slave同步master的数据会出现延时,在master挂掉时可能出现获取ID重复的情况

主主:  如果只是普通的使用自增策略,那这个也会有问题。假设有A/B两个数据库,都是1、2、3自增。那么每个ID都会是重复的。

 

这个时候就要引入一个步长的概念。假设设置MySQL自增策略步长为2。然后其中一个从1开始,另一个从2开始。那么A/B两个数据库自增的ID就会是1、3、5/2、4、6  ; 这样子就很美好了。

可以用这个语句设置, 多个库都用不用的起始值和相同的步长就行了。重要的是有多少个DB就要有多长的步长

 

 

可是这样也存在一个问题,那就是无法扩容

用多主数据库来进行ID自增的话,只要设定好了,就再也无法变更了。以我上边的来说, 想要原基础上添加一个C数据库来进行ID自增,那是做不到的。

这样子就丧失掉了拓展性。虽说在一般的企业应用中这样子一个架构也确实够了 。

 

当然,每次生成ID都去走数据库也怪麻烦的,所以一般也有个ID生成服务,各个应用只需要请求那一个服务就行了。由那个服务去获取ID,达到一个解耦的目的。

这个服务为了高可用也要集群才行,不过这个集群只是为了防特殊情况导致服务挂掉 ,  他们请求的数据源都是一样的。

 

既然可以用MySQL这种关系型数据库,那肯定也可以用Redis这种基于内存的NoSQL数据库来实现这个。

那这个效率不是比关系型数据库高不少?

使用Redis的INCRBY命令就可以从容的在值上增加指定的数字,设置起始值、自增步长。Redis也可以很轻松做到嘛。

但是用Redis的话有一个点要注意:

分布式ID首先得考虑高可用,所以得考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。

而我们只能用AOF,对每一条写命令进行持久化,因为RDB持久化相当于定时打快照来持久化,如果打完快照后,还生成了多次ID,没来得及做下一次快照的时候Redis挂了,重启Redis后就会出现ID重复。

 

3、号段

其实号段也算是数据库ID自增的一种形式。这里只是单独拿出来说

不过就是将原本的一个个获取改成了批量获取,比如直接获取这个数据库里边1 – 10000的ID (只要查出一个数据段落和步长就够了, 自增在自己服务里边自增)。存在分布式ID服务的缓存里边。

这样就免得一次次去和数据库交互。

而且这种方案不再强依赖数据库,就算数据库不可用,那么这个分布式ID服务也能继续支撑一段时间。缺点就是他挂掉重启的话,会丢失一段ID。

之前说过,分布式ID服务的数据源都是一样的。而正是因为数据源一样, 在号段模式里边可能出现那种两个ID服务同时请求一个数据库的同一段数据的情况。那这就很麻烦。

所以这个号段模式需要上一个乐观锁才可以,关于乐观锁可以看我这篇文章:  [浅谈CAS与乐观锁]  http://skypyb.com/2019/08/jishu/961/

 

当然,这种轮子就别自己实现了,人家早就给你安排的明明白白的,只管拿过来用就OK

比如滴滴开源的TinyId:   https://github.com/didi/tinyid

 

4、SnowFlake

这个就是分布式ID界的头头, 著名的雪花算法

对,他是一种算法,是twitter开源的分布式ID生成算法,他和我上边说的数据库生成分布式ID机制完全不一样,最核心的就是它不依赖数据库。

不依赖数据库,这也太美妙了。要知道在软件中每引入一个额外变量都会使架构的复杂度几何上升,SnowFlake无疑就是那分布式ID的最佳实现者。

核心思想是:

分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,那这64个bit的每一位分配如下: 

 

第一个bit位是标识,在java中由于long的最高位是符号位,正数是0,负数是1,由于一般生成的ID为正数,所以固定为0。

时间戳部分占41bit,这个是毫秒级的时间,这个会储存时间戳的差值(当前时间-固定的开始时间),因为这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年

工作机器id占10bit,这里可以很灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点。

序列号部分占12bit,支持同一毫秒内同一个节点可以生成4096个ID

 

根据这个算法的逻辑,只需要将这个算法实现出来,封装为一个工具方法,那么各个业务应用可以直接使用他来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可。直接降低大量复杂度。

关于这个算法的具体实现,搜索引擎搜索SnowFlake,一堆实现。直接复制到自己的项目里边用就完事了。

 

关于雪花算法,其实也有一个小缺点。那就是要为每个机器指定一个机器id, 几十台几百台机子还好,更多那就很操蛋了。

所以有大厂对SnowFlake进行了改造,让其可以自动分配。比如百度的 uid-generator:  https://github.com/baidu/uid-generator

 

结语: 

分布式ID这玩意,其实是个非常简单的东西。

我相信任何一个有点分布式经验的开发者都可以在短时间内理解并在生产中实践。

我就想说一句: 其实用哪个都无所谓。

你要是真的业务有什么东西每天几亿个ID自增,估摸着公司也是流水多多了,轮不到普通程序员操心这玩意。 要是就一普通软件,用户几万几十万的。搞两台Redis来incr也是一样的…

阅读全文

说完了分布式事务最核心的思想(2PC) –> [探秘分布式解决方案: 分布式事务——从核心思想之2PC(两阶段提交)开始]    http://skypyb.com/2019/11/jishu/1149/

 

那么现在进入到更加复杂的场景。像这种跨库调用之类的,一线互联网公司早就不玩这一套东西了。这都9102年了,上来就是微服务架构。

我这么多服务,你整个啥跨库调用呢?一个服务可能同时调用多个其他的服务。这多个其他的服务中都要执行SQL语句,修改落实到服务所对应的数据库之中。

 

 

 

微服务下的服务调用

还是以一个订单支付的流程为例,只不过由之前讲解2PC的单机多库示例改为了微服务示例。

微服务架构下,用户下单的流程将会变成这个样子:

微服务下的订单服务

 

这里边每一个 “服务” 都是一个系统,都是会单独部署的。

可以见得这个复杂度上升的非常大。比如说我用户在下单购买,调用了订单服务的接口,此时订单接口的方法内逻辑不单单只是订单服务自己操作自己的数据库。

订单还得去调用其他的服务的接口。比如库存、积分、仓储 等。

 

那么,在微服务架构下,服务之间互相通过接口进行通信,如何才能在一个 “事务” 中保证数据一致性?

如果出现这种情况   比如订单支付状态已经修改完毕、库存扣除完毕,但是加分却失败了, 这样子咋回滚?

 

那么就轮到新的解决方案出场了。那就是 TCC (Try-Confirm-Cancel)

TCC全名为 两阶段补偿型方案。是现在的互联网大厂主流的分布式事务解决方案之一。而TCC,就是脱胎于2PC,是作为2PC思想在微服务领域的子集被使用着。

所以说为啥2PC是核心思想,不懂2PC,那TCC是肯定明白不了的。 要是懂了TCC的原理,那么前置条件必然是已经懂了2PC。

 

 

 

TCC原理思想

而TCC的原理相比于2PC,有这样一种行为上的变更:

2PC的行为是:  1、预提交 2、实际提交

TCC将其改为了: 1、预留资源 2、确认资源

 

什么叫做预留资源?什么叫做确认资源?

还是以上面我画的的订单活动图为例子,预留的资源,那就是 订单状态、库存数、积分数、出库单

那我们在TCC的方案下进行事物的话,第一阶段就是将这几个资源给他锁定住。然后在第二阶段进行一个资源的确认,即提交/回滚

你如果知道2PC的思想,我想这完全不难以理解。下面是TCC方案的状态图:

TCC-STATE

 

在TCC方案下,在第一阶段中的修改,修改的并不是实际期望的结果,而是期望结果之前的一个中间值。

所以,每个服务都要提供两个接口,一个用于第一阶段的资源预留行为,另一个用于确认/回滚。

(其实你也可以就搞一个接口,然后参数传个对象,由对象中的属性来决定做什么操作也不是不行)

 

比如更改订单,我不可能一下就将这个订单给改成已支付。毕竟现在这个方法还跑到一半,不能完全确定之后的业务是否正常进行。

所以,在此取了个中间态,将订单改为 “更新中” 。

库存、积分等服务也和订单相同。 当所有的接口均调用完成后,即所有的资源预留操作均完成。则在这时进行确认行为。

如果说所有的操作都返回了成功,那么就将订单状态改为已支付,然后将冻结的库存真正减掉、积分真正加上、草稿出库单改为生效出库单。 (confirm)

而反之,若是其中有任意一个接口失败了,那么就对应执行 calcel 操作,将所有改变的状态通通回滚。

 

 

 

如何预留资源

而有的人可能会好奇,什么冻结库存之类的玩意如何实现?

这个其实也很简单,比如你库存表本来只有一个库存数字段。但你现在要用这种TCC方案了,那么就给他再加上一个冻结库存数字段

本来来说,你某个商品库存数是10 , 那么用户再下单完成支付的这个流程中,假设买了1个商品,那么原来的逻辑就得改一下。

正常你买1个商品,那库存就是10-1=9 咯,所以将库存数改为9。而在TCC下,你就不将他改为9,而是在冻结库存数上+1,即数据库状态变为 库存数10,冻结库存数1

那么在其他的用户购买时,你返回给前端的库存数应该就是 库存数10 减去 冻结库存数1 等于现在只有9个库存能够被使用。

用户他不知道冻结不冻结,但是你的程序里边是知道的。用这种逻辑那是一点没有问题。

在你支付接口里边所有的预留操作成功之后,那么就如上所述,将库存字段真正减掉,也就是将10库存真正减1,将冻结库存还原,这是第二阶段该做的事情。

 

说到这里,总没人问第二阶段要是某环节错了怎么办吧? 要是有这疑问的那肯定是没看我上一篇分布式事务文章。

这里也简单的讲一句:本身失败的概率就极小,要是操作失败了肯定是会重试、并且记录事务日志的。状态一个不对劲就由人肉团队、定时任务来进行补偿。

 

 

TCC讲解完毕,那么到底TCC和2PC有着哪些区别呢?

TCC其实说白了,其实也和两阶段提交差不多。就是要开发新的接口。

毕竟是在微服务架构下,复杂度上升很多,要在这种架构下来进行分布式事务本身就要比单机应用考虑的方面更广。

而TCC和2PC之间最主要还有这么一个区别:

在很复杂的、跨了很多数据库的业务场景下,2PC的锁粒度非常大。因为所有的数据库锁均被应用所持有,直到事务完成。性能会很低,根本顶不住大并发。

而TCC的话,从整个系统上来看,吞吐量会好非常多,因为在TCC方案下的的复杂调用链之中,实际上每个数据库操作,都是commit了的,换而言之就是锁释放了并不是一个加在全局的大锁,而是将锁粒度控制在每一个系统的每一个接口里。嘿,有没有想到ConcurrentHashMap。

 

 

 

说在最后

如果将TCC这种解决方案称之为 “术” 。那么2PC就是所谓的”道”

只要你学会了”道”   那么一切基于此道的术相信都难不倒你了。

当然,像这种TCC的实现肯定是有现成的给你用。毕竟自己来搞这么一个框架也是蛮复杂的,不推荐自个造轮子。

业界开源实现也挺多比如ByteTCC、seata、tcc-transaction等,使用方面就不贴了,基本都是配置。

而除了TCC,其实还有像是最大努力通知型方案可靠消息最终一致性方案这些基于最终一致性的分布式事务解决方案。也可以网上搜搜了解一下。

 

阅读全文

为什么要有分布式事务?

本地单机事务,有点工作经验的肯定不陌生,主要用于处理操作量大,复杂度高的数据,一般都由数据库自己实现。

开启一个事务,进行了多个对数据库进行更新(增删改)的语句后,可以自由的选择 commit rollback 来结束事务。

可以从根源上保证多个数据集合的同步。

 

比如一个订单系统,用户支付订单后,订单表的订单状态被改为已支付,同时,库存表中商品剩余数量减一。

若是没有事务机制,在订单库状态修改成功后,由于库存不够减、网络波动、程序bug、数据库挂掉等原因导致数据库中库存减一的SQL语句执行失败; 那这样到最后就会出现实际库存早就没有了,但用户却支付成功的重大事件,即超卖了。

所幸。只要你开了事物,那么事务的原子性确保让你多个语句作为一个原子操作来执行,要么同时成功要么同时失败,在库存-1 失败的同时你订单修改也失败了。所以在单机简单应用中一般不存在这种问题。

 

可是,在稍微复杂一点的场景中,数据库自带的事务机制也有着无能为力的时候。

仍然是一个订单系统,这回场景为由于系统用户激增,入驻商家也越来越多。在日渐庞大的数据量下出于性能考虑决定对数据库进行拆分,由原来的一个库,拆分为一个订单库、一个库存库

你的程序里边,一个下单方法中会执行两个库的SQL语句,在这样的逻辑下,普通的事务还有用么?

就像这样:

订单服务-1

 

那肯定是没有用的。

这种场景中必须要实现跨库事务才能够满足企业级生产需求。

 

要如何实现这种跨库事务? 这里引出分布式事务中的重点思想:阶段提交

 

两阶段提交协议(2PC)

2PC 引入一个事务管理器来作为事务的协调者。

在程序执行复杂数据操作时,开启事务、SQL语句执行完后,先不进行commit。先将执行的结果告知给事务管理器

这个时候,再执行其他数据库的SQL语句。当这个事务中,所有的SQL语句全部执行完毕后,即事务管理器已经得到所有此次事务中SQL语句的执行结果后。

再集体通知所有的数据库进行commit 或 rollback。

 

以下单流程来模拟的话。

前置条件:已引入事务管理器

此时,用户一个支付请求过来。先执行订单库的修改语句,将用户的订单改为已支付。

执行成功后,不提交事务。再开启库存库的事务、执行库存库的库存修改语句,将库存减一。注意,这是两个不同数据库的不同事务。

这个时候数据库的事务隔离机制就将数据锁住了,包括订单库的订单、库存的数量。但是还没有提交。

在每个SQL执行完成之后,都会将执行完成的信息提交给事务管理器。

那么,当订单、库存都修改成功,事务管理器之中都收到了执行成功信息,确认好了本次事务中的所有语句都执行完成后、立即通知所有数据库进行 commit 操作。

任何一个语句执行失败,事务管理器就会通知所有数据库进行rollback。

 

活动图如下所示:

2PC-01

 

2PC中的底层实现思想,就是将原先的单次提交操作分为两个阶段,即 1、预提交 2、实际提交

看起来是不是很简单。大道至简,实际上只要理解了两阶段提交的思想内核,那么任何分布式事务的解决方案都难不倒你了。

因为TCC、消息事务等事务解决方案,都是从2PC衍生的。

 

 

那么问题来了,2PC凭什么可以解决分布式事务问题?

有人可能也有疑问。2PC好像不是那么严谨?

比如: 事务管理器的通知失败了咋搞,假设事务管理器通知订单库的commit成功了,但是通知库存库去commit的时候失败了。那不还是有问题。

这里就必须说明一下分布式事务解决方案的重点。 那就是: 分布式事务不可能100%解决

不信你去问阿里巴巴的员工、Google的员工,去问他们:”你们公司的分布式事务能不能百分百成功呀?” 肯定不可能,起码现在9102年在这个地球上是没有哪家公司敢说自己的分布式事务是百分百成功的。

虽说分布式事务不可能百分百解决,但是可以尽量提高成功概率,比如让你的成功概率接近99.9999%,即让你的系统尽可能的高可用。

 

 

而2PC就是及其有效的提高成功率的方法

这里要先说明,这个事务管理器并不是一个单独的服务,而是内嵌在你程序里的组件。这玩意挂了你程序也挂了,所以不要操心事务管理器挂掉的问题。

以订单为例,其实最容易出现的问题主要是 库存不够减、数据库挂了、网络波动

而在使用2PC的情况下,你的所有SQL都执行成功了,但是事务管理器commit失败了,是个什么概念啊?

要知道在程序的世界里,你别说分两个阶段,你分再多也就几毫秒的时间。

在下单这里预提交成功后,可以显而易见的得出几个结论:

1、所有SQL都执行成功了,所以数据库在执行SQL语句时状态是正常的、服务器和数据库之间的网络也是正常的

2、由于库存库的SQL执行成功,所以库存够减,不可能出现库存不够的问题

 

你跟我说你预提交完,这个时候事务管理器刚通知完订单库commit成功,在提交库存库减库存commit时数据库挂掉了? 或者网络波动导致通知失败了?明明前1ms还好好的?

wdnmd

咳咳,不否认有这种概率啊。可是对于2PC来说,你通知commit失败后,是会重试的。基本上都不会有问题。

那么,要是一直重试都不成功呢?比如挖掘机在你提交库存库commit时,将网线挖断了。emmmmmm,有这种疑问也实属正常,毕竟啥情况都可能发生是不。

这种情况出现确实没有别的办法。一般来说都会记日志打log,之后要么后台通过定时任务补偿,要么人工介入

像一线互联网公司啊,都有大量的一些人工团队专门处理这种极端情况下出现的数据不一致问题,还会让研发工程师写一些什么脚本之类的去回滚、回复数据。

 

 

综上所述。回过头再品一品两阶段提交的思想。为什么说2PC可以提高分布式事务的成功概率,相信已经了然于心了。

而像是这种2PC的具体实现,那肯定也是不用你自己操心的。在java的世界中这种框架已经有不少了。比如开源框架 atomikos,就可以帮你解决这种跨库事务问题,当然,底层就是用的2PC思想做的。

至于如何具体在代码里实现,这里就不贴了。搜索引擎搜一下atomikos可以搜到一大堆。基本上在Spring里边也就是引入依赖后配置一下,然后编程式事务/声明式事务轻松搞定。还可以找一下、了解一下我这篇博客没说明(因为感觉没什么必要)的XA/JTA 接口规范等。

阅读全文

Zookeeper 是一种分布式协调服务,在分布式环境中协调和管理服务是一个复杂的过程。ZooKeeper 通过其简单的架构和 API 解决了这个问题。ZooKeeper 允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

 

分布式协调服务主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成”脏数据”的后果。

为了防止分布式系统中的多个进程之间相互干扰,需要一种分布式协调技术来对这些进程进行调度。

而这个分布式协调技术的核心就是来实现这个分布式锁。

 

分布式锁应该具备哪些条件?

1、在分布式系统环境下,一段代码在同一时间只能被一个机器的一个线程执行

2、高可用的获取锁与释放锁

3、高性能的获取锁与释放锁

4、具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)

5、具备锁失效机制,防止死锁

6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

 

关于业界比较流行的分布式锁实现方案:

一般来说Redis会经常被提及到,但是Redis并不是天生为了实现分布式锁而设计出来的, 他是个NoSql内存数据库;不过我们可以利用一些Redis的特性来实现 分布式锁 这个需求

这个链接文章比较清晰的说了redis如何实现分布式锁和redis分布式锁面临的问题,这个说的很清楚了我就不详细说了。

http://zhangtielei.com/posts/blog-redlock-reasoning.html

 

那Zookeeper呢 ,雅虎工程师设计Zookeeper的初衷,就是为了实现分布式锁服务的,利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。当然Zookeeper不止这个功能,比如它可以用来实现服务发现与注册的功能,单以这个需求来使用Zookeeper的也是很多的。

 

在说Zookeeper实现分布式锁之前,首先来讲下他为啥可以实现分布式锁,以及Zookeeper的数据模型:

Zookeeper 的数据模型很像数据结构当中的树,也很像文件系统的目录。

树是由节点所组成,Zookeeper 的数据存储也同样是基于节点,这种节点叫做 Znode,但是,不同于树的节点,Znode 的引用方式是路径引用,类似于文件路径:/car/bmw   、/animal/cat

这样的层级结构,让每一个 Znode 节点拥有唯一的路径。

 

那么Znode 包含哪些元素呢?有这么些:

  • data:Znode 存储的数据信息。
  • ACL:记录 Znode 的访问权限,即哪些人或哪些 IP 可以访问本节点。
  • stat:包含 Znode 的各种元数据,比如事务 ID、版本号、时间戳、大小等等。
  • child:当前节点的子节点引用

Zookeeper 是为读多写少的场景所设计。Znode 并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点数据最大只有1M。

关于Zookeeper的基本增删改查的操作就不详细说明了,主要有这么几种:create、delete、exists、getData、setData、getChildren。

而跟增删改查息息相关的,Zookeeper 客户端在请求读操作的时候,可以选择是否设置 Watch,也就是Zookeeper的事件通知机制

 

可以把 Watch 理解成是注册在特定 Znode 上的触发器。调用了 create、delete、setData 方法的时候 (做出修改操作时),将会触发 Znode 上注册的事件,请求 Watch 的客户端会接收到异步通知。

大概的事件通知机制如下:

  • 客户端调用 getData 方法,watch 参数是 true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
  • 当被 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。

 

好,说了这么多,差不过该进入主题如何实现分布式锁了,上边说了利用 Zookeeper 的顺序临时节点可以实现分布式锁,那么这顺序临时节点又是个什么玩意?

默认创建的Znode  是”持久节点” 创建节点的客户端与 Zookeeper 断开连接后,该节点依旧存在。

除此之外,还有其他几种,其实看名字也能猜出是啥用了:

持久顺序节点:  所谓持久顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号。其他和持久节点一样。

临时节点:  和持久节点相反,当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。

临时顺序节点: 顾名思义,临时节点和顺序节点的合成体;在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 Zookeeper 断开连接后,节点会被删除。

 

Zookeeper 分布式锁的原理机制:

说完节点,也就差不多可以实现分布式锁了,其实知道这几个节点是个什么玩意后,在结合一下Watch机制,基本可以在脑内猜想个实现大概出来。

加锁释放锁流程:

1、首先,在 Zookeeper 当中创建一个持久节点,给它取个名字名字叫 ParentLock。当第一个客户端想要获得锁时,在 ParentLock 这个节点下面创建一个临时顺序节点 Lock1。之后,Client1 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock1 是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

2、这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下再创建一个临时顺序节点 Lock2。

3、Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现节点 Lock2 并不是最小的。于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态,这就形成了一个等待队列。

4、  当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。此时由于 Client2 一直监听着 Lock1 的存在状态,当 Lock1 节点被删除,Client2 会立刻收到通知。这时候 Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的节点 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章获得了锁。

5、如果说获得锁的 Client1 在任务执行过程中如果崩溃了,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除;所以Client2 又收到通知获得了锁。

 

说到这,是不是觉得Zookeeper的机制来实现分布式锁较为不错,人家都给你封装好了,不用向Redis那样实现分布式锁还得考虑超时、原子性、误删除之类的。

还有等待队列可以提升抢锁效率,比起Redis实现的效果确实是舒服了许多。

 

阅读全文
EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00