浅谈ZooKeeper分布式锁(来自LJ)

xiaoxiao2021-03-01  19

目前的应用系统,大部分都是分布式部署的,分布式应用中的数据一致性问题是每个系统都要面临的问题,分布式的CAP理论告诉我们“一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足两项。”,所以在对一个分布式系统架构设计的过程中,往往会在系统的可用性和数据一致性之前进行反复的权衡,在很多场景中我们牺牲了强一致性,选择最终一致性,来达到系统高可用的目标。所谓最终一致性,其实就是降低了标准的一致性,即以”数据一致性存在延迟时间”来换取数据读写的高性能,目前最终一致性基本成为越来越多的分布式系统所遵循的一个设计目标。

现在有很多技术方案可以支持最终一致性,比如基于事务型消息队列的最终一致性,基于消息队列加定时补偿机制的最终一致性,业务系统业务逻辑的commit/rollback机制,异步回调机制,业务应用系统的幂等性控制等等,本文主要探讨幂等性控制解决方案中的分布式锁,所谓分布式锁就是分布式应用各节点对共享资源的排他式访问而设定的锁,用来实现多节点的最终一致性。目前比较常用的有以下几种方案:数据库实现的分布式锁,缓存redis实现的分布式锁,ZooKeeper实现的分布式锁,本文主要探讨基于ZooKeeper实现的分布式锁。

1.主要知识点

1.1 ZooKeeper的节点

在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短,取决于数据节点的节点类型,节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),和顺序节点(SEQUENTIAL )三大类,其中,持久节点是指一旦这个ZNode被创建,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在ZooKeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端的所有临时节点都会被移除。 具体在节点创建过程中,通过组合使用,可以生成 4 种节点类型:持久节点(PERSISTENT),持久顺序节点(PERSISTENT_SEQUENTIAL),临时节点(EPHEMERAL),临时有序节点(EPHEMERAL_SEQUENTIAL)。

1.2 ZooKeeper的Watcher机制

这里特别强调一下,ZooKeeper的Watcher机制:一个Watcher事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时,服务器将这个改变发送给设置了Watcher的客户端,接着会将这个Watcher从session中删除,因此,如果想继续监控,必须重新注册。当前ZooKeeper有如下四种Watch事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更。

2.主要步骤

1.每当进程需要访问共享资源时,客户端调用create()方法创建名为“disLock/lock”的节点,需要注意的是,这里节点的创建类型需要设置为临时有序节点。 2.在建立子节点后,客户端调用getChildren(“disLock”)方法来获取所有已经创建的子节点,注意此时不用设置任何Watcher。 3.如果发现自己在步骤1中创建的节点序号最小,那么就认为这个客户端获得了锁。 4.假如不是序号最小的节点,就获得该节点的上一顺序节点,并给该节点是否存在注册监听事件。同时在这里阻塞,等待监听事件的发生,获得锁控制权。 5.当调用完共享资源后,删除被关注的临时节点,进而可以引发监听事件,释放该锁。

3.主要优点

1.ZooKeeper分布式锁使用临时节点可以有效的解决锁无法释放的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。 2.ZooKeeper分布式锁支持Watcher机制,这样实现阻塞锁,可以watch锁数据,等到数据被删除,ZooKeeper会通知客户端去重新竞争锁。 3.ZooKeeper可以有效的解决单点问题,ZooKeeper是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

4.分布式锁实践

可以直接使用ZooKeeper第三方库Curator客户端,Curator提供了可重入锁Shared Reentrant Lock,不可重入锁Shared Lock,可重入读写锁Shred Reentrant Read Write Lock,信号量Shared Semaphore,多锁对象 Multi Shared Lock。 本文主要描述最常用的可重入锁Shared Reentrant Lock,是由Curator提的InterProcessMutex实现的,下面我们来看一下InterProcessMutex的用法。

4.1创建

 

1

2

3

4

5

         public InterProcessMutex(CuratorFramework client, String path)

         {

                  this(client, path, new StandardLockInternalsDriver());

         }

 

 

client是ZooKeeper客户端链,path是加锁的ZooKeeper节点path。 InterProcessMutex实例是一个可重用的对象。不需要每次使用时创建一个新的实例,可以安全的使用单例。你可以在一个线程中多次调用acquire,在线程拥有锁时它总是返回true,具体实现原理可以查看源码。

4.2 使用

4.21 获得锁 如果想要获得锁,则需要在下列acquire方法中,选择一个调用: (1) 永久阻塞

 

1

2

3

4

5

6

7

8

          public void acquire() throws Exception

          {

             if ( !internalLock(-1, null) )

             {

                   throw new IOException("Lost connection while trying to acquire lock: " + basePath);

             }

          }

 

 

此方法,会申请获得一个互斥锁。并且会一直阻塞,直到获得可用锁为止。 注意:同一个已持有锁的线程可以直接重入的。每一此锁用完后都应该释放,也即是每一个acquire方法都应该有一个对应的release方法。

(2) 限期阻塞

 

1

2

3

4

5

          public boolean acquire(long time, TimeUnit unit) throws Exception

          {

                 return internalLock(time, unit);

          }

 

 

和上面的方法类似。区别在于,此方法不会永久阻塞,而是在超过等待期限后,返回是否获得锁的结果。

4.22 释放锁

 

1

2

          public void release()

 

 

持有锁的线程如果调用此方法,就会释放持有的锁。但是,要注意,如果线程多次调用了acquire方法,那么也应该调用相同次数的 release方法,否则线程会继续持有锁。

4.23 撤销 InterProcessMutex支持一种协商撤销互斥锁的机制,可以用于死锁的情况,想要撤销一个互斥锁可以调用下面这个方法:

 

1

2

3

4

5

          public void makeRevocable(RevocationListener listener)

          {

               makeRevocable(listener, MoreExecutors.sameThreadExecutor());

          }

 

 

这个方法可以让锁持有者来处理撤销动作。 当其他进程/线程想要你释放锁时,就会回调参数中的监听器方法。 但是,此方法不是强制撤销 的,是一种协商机制。 当想要去撤销/释放一个锁时,可以通过Revoker中的静态方法来发出请求:

 

1

2

3

4

5

6

7

8

9

10

11

12

            public static void  attemptRevoke(CuratorFramework client, String path) throws Exception

           {

                 try

                  {

                      client.setData().forPath(path, LockInternals.REVOKE_MESSAGE);

                  }

                  catch ( KeeperException.NoNodeException ignore )

                  {

                      // ignore

                  }

           }

 

 

path是加锁节点,通常可以用InterProcessMutex.getParticipantNodes()获得。 这个方法会发出撤销某个锁的请求。如果锁的持有者注册了上述的RevocationListener监听器,那么就会调用监听器方法协商撤销锁。

4.24 错误处理 如下方法:

 

1

2

3

4

5

6

7

8

9

           void registerListeners(CuratorFramework client) {

                     lient.getConnectionStateListenable().addListener(new ConnectionStateListener() {

                               @Override

                               public void stateChanged(CuratorFramework client, ConnectionState newState) {

                                           //处理逻辑

                                }

                        });

                    }

 

 

推荐使用ConnectionStateListener用以处理连接异常的情况。当ZooKeeper连接状态的变化出现异常时, 将通过ConnectionStateListener 接口进行监听, 并进行相应的处理, 这些异常状态变化包括:   (1)暂停(SUSPENDED): 当连接丢失, 将暂停所有操作, 直到连接重新建立, 如果在规定时间内无法建立连接, 将触发LOST通知。  (2)重连(RECONNECTED): 连接丢失, 执行重连时, 将触发该通知  (3)丢失(LOST): 连接超时时, 将触发该通知。 如果遇到链接中断SUSPENDE,在恢复链接RECONNECTED之前,就不能保证是不是还持有锁了,而如果链接丢失LOST,那就意味着不再 持有锁了。

5.总结

在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常用的解决方案就是使用分布式锁,使用ZooKeeper实现分布式锁可以有效有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题,实现起来较为简单。但是使用ZooKeeper实现的分布式锁需要对ZooKeeper的原理有所了解。

转载请注明原文地址: https://www.6miu.com/read-3450366.html

最新回复(0)