zookeeper怎么实现分布式锁

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了zookeeper怎么实现分布式锁相关的知识,希望对你有一定的参考价值。

1. 利用节点名称的唯一性来实现共享锁
ZooKeeper抽象出来的节点结构是一个和unix文件系统类似的小型的树状的目录结构。ZooKeeper机制规定:同一个目录下只能有一个唯一的文件名。例如:我们在Zookeeper目录/test目录下创建,两个客户端创建一个名为Lock节点,只有一个能够成功。
算法思路: 利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。
基于以上机制,利用节点名称唯一性机制的共享锁算法流程如图所示:

该共享锁实现很符合我们通常多个线程去竞争锁的概念,利用节点名称唯一性的做法简明、可靠。
由上述算法容易看出,由于客户端会同时收到/test/Lock被删除的通知,重新进入竞争创建节点,故存在"惊群现象"。
使用该方法进行测试锁的性能列表如下:

总结 这种方案的正确性和可靠性是ZooKeeper机制保证的,实现简单。缺点是会产生“惊群”效应,假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,仅仅有一个客户端得到锁。

2. 利用临时顺序节点实现共享锁的一般做法
首先介绍一下,Zookeeper中有一种节点叫做顺序节点,故名思议,假如我们在/lock/目录下创建节3个点,ZooKeeper集群会按照提起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003。
ZooKeeper中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZooKeeper集群断开连接,则开节点自动被删除。
利用上面这两个特性,我们来看下获取实现分布式锁的基本逻辑:
客户端调用create()方法创建名为“locknode/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。
客户端调用getChildren(“locknode”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher。
客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
上面这个分布式锁的实现中,大体能够满足了一般的分布式集群竞争锁的需求。这里说的一般性场景是指集群规模不大,一般在10台机器以内。
不过,细想上面的实现逻辑,我们很容易会发现一个问题,步骤4,“即获取所有的子点,判断自己创建的节点是否已经是序号最小的节点”,这个过程,在整个分布式锁的竞争过程中,大量重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知——这个显然看起来不怎么科学。客户端无端的接受到过多的和自己不相关的事件通知,这如果在集群规模大的时候,会对Server造成很大的性能影响,并且如果一旦同一时间有多个节点的客户端断开连接,这个时候,服务器就会像其余客户端发送大量的事件通知——这就是所谓的惊群效应。而这个问题的根源在于,没有找准客户端真正的关注点。
我们再来回顾一下上面的分布式锁竞争过程,它的核心逻辑在于:判断自己是否是所有节点中序号最小的。于是,很容易可以联想的到的是,每个节点的创建者只需要关注比自己序号小的那个节点。

3、利用临时顺序节点实现共享锁的改进实现
下面是改进后的分布式锁实现,和之前的实现方式唯一不同之处在于,这里设计成每个锁竞争者,只需要关注”locknode”节点下序号比自己小的那个节点是否存在即可。
算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。
对于解锁操作,只需要将自身创建的节点删除即可。
具体算法流程如下图所示:

使用上述算法进行测试的的结果如下表所示:

该算法只监控比自身创建节点序列号小(比自己小的最大的节点)的节点,在当前获得锁的节点释放锁的时候没有“惊群”。
总结 利用临时顺序节点来实现分布式锁机制其实就是一种按照创建顺序排队的实现。这种方案效率高,避免了“惊群”效应,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒。

4、使用menagerie
其实就是对方案3的一个封装,不用自己写代码了。直接拿来用就可以了。
menagerie基于Zookeeper实现了java.util.concurrent包的一个分布式版本。这个封装是更大粒度上对各种分布式一致性使用场景的抽象。其中最基础和常用的是一个分布式锁的实现: org.menagerie.locks.ReentrantZkLock,通过ZooKeeper的全局有序的特性和EPHEMERAL_SEQUENTIAL类型znode的支持,实现了分布式锁。具体做法是:不同的client上每个试图获得锁的线程,都在相同的basepath下面创建一个EPHEMERAL_SEQUENTIAL的node。EPHEMERAL表示要创建的是临时znode,创建连接断开时会自动删除; SEQUENTIAL表示要自动在传入的path后面缀上一个自增的全局唯一后缀,作为最终的path。因此对不同的请求ZK会生成不同的后缀,并分别返回带了各自后缀的path给各个请求。因为ZK全局有序的特性,不管client请求怎样先后到达,在ZKServer端都会最终排好一个顺序,因此自增后缀最小的那个子节点,就对应第一个到达ZK的有效请求。然后client读取basepath下的所有子节点和ZK返回给自己的path进行比较,当发现自己创建的sequential node的后缀序号排在第一个时,就认为自己获得了锁;否则的话,就认为自己没有获得锁。这时肯定是有其他并发的并且是没有断开的client/线程先创建了node。
参考技术A 很多使用Zookeeper的情景是需要我们嵌入Zookeeper作为自己的分布式应用系统的一部分来提供分布式服务,此时我们需要通过程序的方式来启动Zookeeper。此时可以通过Zookeeper API的ZooKeeperServerMain类来启动Zookeeper服务。 参考技术B Apache Zookeeper是我最近遇到的最酷的技术,我是在研究Solr Cloud功能的时候发现的。Solr的分布式计算让我印象深刻。你只要开启一个新的实例就能自动在Solr Cloud中找到。它会将自己分派到某个分片中,并确定出自己是一个Leader(源)还是一个副本。不一会儿,你就可以在你的那些服务器上查询到了。即便某些服务器宕机了也可以继续工作。非常动态、聪明、酷。

将运行多个应用程序作为一个逻辑程序并不是什么新玩意。事实上,我在几年前就已写过类似的软件。这种架构比较让人迷惑,使用起来也费劲。为此Apache Zookeeper提供了一套工具用于管理这种软件。

为什么叫Zoo?“因为要协调的分布式系统是一个动物园”。

在本篇文章中,我将说明如何使用PHP安装和集成Apache ZooKeeper。我们将通过service来协调各个独立的PHP脚本,并让它们同意某个成为Leader(所以称作Leader选举)。当Leader退出(或崩溃)时,worker可检测到并再选出新的leader。

ZooKeeper是一个中性化的Service,用于管理配置信息、命名、提供分布式同步,还能组合Service。所有这些种类的Service都会在分布式应用程序中使用到。每次编写这些Service都会涉及大量的修bug和竞争情况。正因为这种编写这些Service有一定难度,所以通常都会忽视它们,这就使得在应用程序有变化时变得难以管理应用程序。即使处理得当,实现这些服务的不同方法也会使得部署应用程序变得难以管理。

虽然ZooKeeper是一个Java应用程序,但C也可以使用。这里就有个PHP的扩展,由Andrei Zmievski在2009创建并维护。你可以从PECL中下载,或从GitHub中直接获取PHP-ZooKeeper。
要使用该扩展你首先要安装ZooKeeper。可以从官方网站下载。
$ tar zxfv zookeeper-3.4.5.tar.gz
$ cd zookeeper-3.4.5/src/c
$ ./configure --prefix=/usr/
$ make
$ sudo make install

这样就会安装ZooKeeper的库和头文件。现在准备编译PHP扩展。

$ cd$ git clone https://github.com/andreiz/php-zookeeper.git
$ cd php-zookeeper
$ phpize
$ ./configure
$ make
$ sudo make install

将“zookeeper.so”添加到PHP配置中。

$ vim /etc/php5/cli/conf.d/20-zookeeper.ini

因为我不需要运行在web服务环境下,所以这里我只编辑了CLI的配置。将下面的行复制到ini文件中。

extension=zookeeper.so

使用如下命令来确定扩展是否已起作用。

$ php -m | grep zookeeper
zookeeper

现在是时候运行ZooKeeper了。目前唯一还没有做的是配置。创建一个用于存放所有service数据的目录。
$ mkdir /home/you-account/zoo
$ cd$ cd zookeeper-3.4.5/
$ cp conf/zoo_sample.cfg conf/zoo.cfg
$ vim conf/zoo.cfg

找到名为“dataDir”的属性,将其指向“/home/you-account/zoo”目录。

$ bin/zkServer.sh start
$ bin/zkCli.sh -server 127.0.0.1:2181[zk: 127.0.0.1:2181(CONNECTED) 14] create /test 1
Created /test[zk: 127.0.0.1:2181(CONNECTED) 19] ls /[test, zookeeper]

此时,你已成功连到了ZooKeeper,并创建了一个名为“/test”的znode(稍后我们会用到)。ZooKeeper以树形结构保存数据。这很类似于文件系统,但“文件夹”(译者注:这里指非最底层的节点)又和文件很像。znode是ZooKeeper保存的实体。Node(节点)的说法很容易被混淆,所以为了避免混淆这里使用了znode。

因为我们稍后还会使用,所以这里我们让客户端保持连接状态。开启一个新窗口,并创建一个zookeeperdemo1.php文件。

<?php
class ZookeeperDemo extends Zookeeper

public function watcher( $i, $type, $key )
echo "Insider Watcher\n";

// Watcher gets consumed so we need to set a new one
$this->get( '/test', array($this, 'watcher' ) );


$zoo = new ZookeeperDemo('127.0.0.1:2181');$zoo->get( '/test', array($zoo, 'watcher' ) );
while( true )
echo '.';
sleep(2);

现在运行该脚本。

$ php zookeeperdemo1.php

此处应该会每隔2秒产生一个点。现在切换到ZooKeeper客户端,并更新“/test”值。

[zk: 127.0.0.1:2181(CONNECTED) 20] set /test foo

这样就会静默触发PHP脚本中的“Insider Watcher”消息。怎么会这样的?

ZooKeeper提供了可以绑定在znode的监视器。如果监视器发现znode发生变化,该service会立即通知所有相关的客户端。这就是PHP脚本如何知道变化的。Zookeeper::get方法的第二个参数是回调函数。当触发事件时,监视器会被消费掉,所以我们需要在回调函数中再次设置监视器。

现在你可以准备创建分布式应用程序了。其中的挑战是让这些独立的程序决定哪个(是leader)协调它们的工作,以及哪些(是worker)需要执行。这个处理过程叫做leader选举,在ZooKeeper Recipes and Solutions你能看到相关的实现方法。

这里简单来说就是,每个处理(或服务器)紧盯着相邻的那个处理(或服务器)。如果一个已被监视的处理(也即Leader)退出或者崩溃了,监视程序就会查找其相邻(此时最老)的那个处理作为Leader。

在真实的应用程序中,leader会给worker分配任务、监控进程和保存结果。这里为了简化,我跳过了这些部分。
创建一个新的PHP文件,命名为worker.php。

<?php
class Worker extends Zookeeper

const CONTAINER = '/cluster';

protected $acl = array(
array(
'perms' => Zookeeper::PERM_ALL,
'scheme' => 'world',
'id' => 'anyone' ) );
private $isLeader = false;

private $znode;

public function __construct( $host = '', $watcher_cb = null, $recv_timeout = 10000 )
parent::__construct( $host, $watcher_cb, $recv_timeout );


public function register()
if( ! $this->exists( self::CONTAINER ) )
$this->create( self::CONTAINER, null, $this->acl );


$this->znode = $this->create( self::CONTAINER . '/w-',
null,
$this->acl,
Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE );

$this->znode = str_replace( self::CONTAINER .'/', '', $this->znode );

printf( "I'm registred as: %s\n", $this->znode );

$watching = $this->watchPrevious();

if( $watching == $this->znode )
printf( "Nobody here, I'm the leader\n" );
$this->setLeader( true );
else
printf( "I'm watching %s\n", $watching );



public function watchPrevious()
$workers = $this->getChildren( self::CONTAINER );
sort( $workers );
$size = sizeof( $workers );
for( $i = 0 ; $i < $size ; $i++ )
if( $this->znode == $workers[ $i ] )
if( $i > 0 )
$this->get( self::CONTAINER . '/' . $workers[ $i - 1 ], array( $this, 'watchNode' ) );
return $workers[ $i - 1 ];


return $workers[ $i ];



throw new Exception( sprintf( "Something went very wrong! I can't find myself: %s/%s",
self::CONTAINER,
$this->znode ) );


public function watchNode( $i, $type, $name )
$watching = $this->watchPrevious();
if( $watching == $this->znode )
printf( "I'm the new leader!\n" );
$this->setLeader( true );

else
printf( "Now I'm watching %s\n", $watching );


public function isLeader()
return $this->isLeader;


public function setLeader($flag)
$this->isLeader = $flag;


public function run()
$this->register();

while( true )
if( $this->isLeader() )
$this->doLeaderJob();

else
$this->doWorkerJob();


sleep( 2 );



public function doLeaderJob()
echo "Leading\n";


public function doWorkerJob()
echo "Working\n";


$worker = new Worker( '127.0.0.1:2181' );$worker->run();

打开至少3个终端,在每个终端中运行以下脚本:

# term1
$ php worker.php
I'm registred as: w-0000000001Nobody here, I'm the leader
Leading
# term2
$ php worker.php
I'm registred as: w-0000000002I'm watching w-0000000001
Working
# term3
$ php worker.php
I'm registred as: w-0000000003I'm watching w-0000000002
Working

现在模拟Leader崩溃的情形。使用Ctrl+c或其他方法退出第一个脚本。刚开始不会有任何变化,worker可以继续工作。后来,ZooKeeper会发现超时,并选举出新的leader。

虽然这些脚本很容易理解,但是还是有必要对已使用的Zookeeper标志作注释。
$this->znode = $this->create( self::CONTAINER . '/w-', null, $this->acl, Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE );

每个znode都是EPHEMERAL和SEQUENCE的。

EPHEMRAL代表当客户端失去连接时移除该znode。这就是为何PHP脚本会知道超时。SEQUENCE代表在每个znode名称后添加顺序标识。我们通过这些唯一标识来标记worker。

在PHP部分还有些问题要注意。该扩展目前还是beta版,如果使用不当很容易发生segmentation fault。比如,不能传入普通函数作为回调函数,传入的必须为方法。我希望更多PHP社区的同仁可以看到Apache ZooKeeper的好,同时该扩展也会获得更多的支持。

ZooKeeper是一个强大的软件,拥有简洁和简单的API。由于文档和示例都做的很好,任何人都可以很容易的编写分布式软件。让我们开始吧,这会很有趣的。

漫谈分布式锁之ZooKeeper实现

笔耕墨耘,深研术道
漫谈分布式锁之ZooKeeper实现
0 1
导读
  • 为什么ZooKeeper可以实现分布式锁
  • 基于ZooKeeper实现排他锁

  • 基于ZooKeeper实现共享锁

  • Curator对分布式锁的支持

  • 如何选择合适的分布式锁
0 2
写在前面

本篇文章的大多内容,来自《从Paxos到ZooKeeper分布式一致性原理与实战》,所以它更像一篇笔记,但是在此基础上进行了整合和补充说明。

ZooKeeper是什么?ZooKeeper是典型的分布式数据一致性的解决方案。本文中,我们关注下其在分布式锁方面的实战。

在分布式系统中,应用程序可以基于ZooKeeper实现诸如配置中心、命名服务、同步工具(分布式锁和分布式队列)、负载均衡、数据发布/订阅、分布式协调/通知、集群管理和Master选举等功能。

0 3
为什么ZooKeeper可以实现分布式锁

1. ZooKeeper可以保证如下分布式一致性特性

顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去。

原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。

单一视图:无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的。

可靠性:一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。

实时性:ZooKeeper保证在一定时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。(最终一致性)

2. ZooKeeper的一致性协议

在ZooKeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。这也在一定程度上保证了分布式锁的可靠性。

3. 节点属性

ZooKeeper节点:

节点类型
描述
PERSISTENT 持久节点
PERSISTENT_SEQUENTIAL 持久顺序节点
EPHEMERAL 临时节点(不可在拥有子节点)
EPHEMERAL_SEQUENTIAL 临时顺序节点(不可在拥有子节点)

基于ZooKeeper作为分布式协调器的一致性特点和znode临时顺序节点的特性,满足我们对分布式锁实现的条件。

0 4
基于ZooKeeper实现排他锁

排他锁(Exclusive Locks,简称X锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作直到T1释放了排他锁。

锁的定义

在ZooKeeper中,我们用一个节点来表示锁,如:/exclusive_lock/lock节点,就可以被定义为一个锁。

漫谈分布式锁之ZooKeeper实现

获取锁

在需要获取排他锁的时候,所有的客户端都会试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock;

ZooKeeper会保证在所有的客户端中,最终只有一个客户端能够创建成功,那么就认为其获取到锁。

释放锁

在定义锁的部分,我们提到,/exclusive_lock/lock是一个临时节点,因此在以下两种情况下,都有可能释放锁。

  • 当前获取锁的客户端机器发生宕机,那么ZooKeeper上的这个临时节点就会被移除;

  • 正常执行业务逻辑后,客户端会主动将自己创建的临时节点删除。

无论在什么情况下移除了/lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”的过程。整个排他锁的获取和释放过程如下图所示:

漫谈分布式锁之ZooKeeper实现

上述加锁和释放锁的流程存在羊群效应:无论在什么情况下移除了/lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端,假如客户端多了的话,将会有大量的“Watcher通知”和“尝试获取锁”的操作,这带来了巨大的性能消耗。

羊群效应优化

使用临时序号节点优化;只有当自己所创建的节点是最小的时候才能获取锁,如此,只需要监听比自己次小的节点即可。如下:


漫谈分布式锁之ZooKeeper实现

上述方案,就是优化后的排他锁,也是常用的一种解决方案。

0 5
基于ZooKeeper实现共享锁

共享锁(Shared Locks,简称S锁),又称为读锁,同样是一种基本的锁类型。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。

共享锁和排他锁的根本区别:加上排他锁后,数据对象只对一个事务可见,而加上共享锁后,数据对象对所有事务都可见。

锁的定义

和排他锁类似,共享锁依然是ZooKeeper上的一个节点,共享锁的节点设计类似“/shared_lock/[host]-请求类型-序号”的临时序号节点,例如/shared_lock/host1-R-000001,那么这个节点就代表一个共享锁,如下图所示:

漫谈分布式锁之ZooKeeper实现

获取锁

在需要获取共享锁时候,所有客户端都会到/shared_lock这个节点下面创建一个临时顺序节点,如果当前请求是读请求,那么就创建例如/shared_lock/host1-R-000001的节点;如果是写请求,那么就创建如/shared_lock/host1-W-000001的节点。

说明:在同一个持久节点下面创建的多个临时顺序节点,,ZooKeeper会维持一个递增顺序,如下:

漫谈分布式锁之ZooKeeper实现

判断读写顺序

根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。基于这个原则,我们通过zk的节点来确定分布式读写顺序,大致如下5个步骤:

  1. 客户端创建一个类似于“/shared_lock/[host]-请求类型-序号”的临时顺序节点;

  2. 客户端获取/shared_lock节点下所有已创建的子节点列表,注意这里不注册任何Watcher;

  3. 确定自己的节点序号在所有子节点中的顺序;

  4. 如果无法获取共享锁(下面注释说明该情况,那么就对比自己序号小的那个子节点注册监听Watcher。注意,这里“比自己序号小的子节点”只是一个笼统的说法,具体需要区分读请求和写请求:

    对于读请求:向比自己序号小的最后一个写请求节点注册Watcher监听;

    对于写请求:向比自己序号小的最后一个节点注册Watcher监听。

  5. 等待Watcher通知,继续进入步骤2。

对于第4点,有如下说明:

什么是无法获取共享锁? 


对于读请求:

如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑; 

如果比自己序号小的子节点中有写请求,那么就需要进入等待; 


对于写请求: 

如果自己不是序号最小的子节点,那么就需要进入等待。

释放锁

释放锁的逻辑和排他锁一样,这里不再赘述。整个共享锁的获取和释放(考虑了羊群效应)如下图所示:

共享锁的实现较为复杂。实际业务场景中往往根据业务场景来选择,对于写操作比较频繁的场景,建议使用排他锁;对于读多写少的场景,可以使用共享锁。

上面,我们了解了基于ZooKeeper实现分布式锁的原理,但是实际上基于ZooKeeper自己实现一个生产级别的分布式锁,还是比较麻烦的,下面,我们可以借助Curator来使用其提供的分布式锁,它的原理大致和上面所讨论的类似,其实现考虑了可重入性、Session expiration和失败容错等问题。

0 6
Curator对分布式锁的支持
Curator是什么

Apache Curator是用于Apache ZooKeeper(一种分布式协调服务)的Java / JVM客户端库。

Apache Curator is a Java/JVM client library for Apache ZooKeeper, a distributed coordination service.

Curator对锁的支持

InterProcessMutex:可重入排它锁,公平锁
InterProcessSemaphoreMutex:不可重入互斥锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:分布式联锁

Curator分布式锁的使用

其使用比较简单,可以参考GitHub:

https://github.com/apache/curator/tree/master/curator-examples/src/main/java/locking

Curator分布式锁原理探究

限于篇幅和可读性考虑,将在另外的篇章分析Curator的源码及其细节内容。

0 7
如何选择合适的分布式锁
基于Redis的分布式锁的优缺点

优点:

  • 相对高效率,高性能(相对于数据库的实现);

劣势:

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

  • 即便使用Redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题;

  • 有人提及Redis在等待获取锁的时候进行自旋,消耗性能;其实这个问题在Redisson中得到了解决:如果当前线程无法获取锁,将会阻塞一段时间(锁过期的时间)后,继续去尝试获取锁,这在一定程度上优化了自旋带来的性能消耗。

基于Zookeeper的分布式锁的优缺点

优点:

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

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

劣势:

  • 如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

0 8
总结
  1. ZooKeeper分布式锁实现简单,由于其基于CP模型,集群部署时,其内部数据同步需要消耗一定资源,因此一般适合于并发量小的场景使用,例如定时任务的运行等;

  2. Redis分布式锁(非Redlock)由于Redis自己的高性能原因,会有很好的性能,但是极端情况下会存在两个客户端获取锁(可以通过监控leader故障和运维措施来缓解和解决该问题),因此适用于高并发的场景;

  3. Database分布式锁由于数据库本身的限制:性能不高且不满足高可用(即使存在备份,也会导致数据不一致),因此,工作中很难见到真正使用数据库来作为分布式锁的解决方案,这里使用数据库实现主要是为了理解分布式锁的实现原理。

在实际开发过程中,建议采用Redisson实现分布式锁。

往期回顾:基于数据库和基于缓存的分布式锁实现



0 9
引用
[1] https://cwiki.apache.org/confluence/display/ZOOKEEPER/Zab

[2] https://dl.acm.org/doi/10.1145/1529974.1529978

[3] https://curator.apache.org/

[4] https://cwiki.apache.org/confluence/display/CURATOR

[5] https://github.com/apache/curator/tree/master/curator-examples/src/main/java/locking

[6] 倪超,《从Paxos到ZooKeeper分布式一致性原理与实战》,电子工业出版社

  分享是一种积极的生活态度  

以上是关于zookeeper怎么实现分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

Zookeeper怎么实现分布式锁?

分布式锁06-Zookeeper实现分布式锁:可重入锁源码分析

分布式锁06-Zookeeper实现分布式锁:可重入锁源码分析

ZooKeeper学习ZooKeeper实现分布式锁

中间件Zookeeper 从 0 到 1 实现一个分布式锁

Redis实现分布式锁与Zookeeper实现分布式锁区别