Zookeeper分布式锁,小小节点真奇妙

    科技2022-08-12  91

    文章目录

    一、前言二、从锁到分布式锁2.1 分布式技术四个分支2.2 从锁到分布式锁(为什么redis需要分布式锁)2.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建2.4 zookeeper四种节点 三、进程内无锁--进程内加锁--zookeeper分布式锁--zookeeper分布式锁3.1 进程内不是锁,造成线程安全问题3.2 进程内使用锁(synchronized / lock)3.3 zookeeper分布式锁(加锁+解锁+监听)3.4 zookeeper分布式锁(加锁+解锁+监听+解决羊群效应) 四、面试金手指4.1 分布式 + 从锁到分布式锁 + zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建4.1.1 分布式技术四个分支4.1.2 从锁到分布式锁(为什么redis需要分布式锁)4.1.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建 4.2 zookeeper是如何基于节点去实现各种分布式锁的? 五、小结

    一、前言

    二、从锁到分布式锁

    2.1 分布式技术四个分支

    分布式相关技术:分布式部署导致分布式锁、分布式事务、分布式登录(单点登录)、分布式存储HDFS四个技术的出现 1、后端服务分布式部署后,进程内Java并发多线程锁(阻塞锁+CAS)不适用了,出现了分布式锁(三种实现方式:直接在Mysql表上实现分布式锁、引入一个中间件zookeeper实现各服务之间分布式锁、引入一个中间件redis实现各服务之间分布式锁) 2、mysql分布式部署后,事务不适用了,出现了分布式事务(七种实现方式:两阶段、三阶段、XA、最大努力、ebay本地消息表、TCC编程模式、半消息/最终一致性) 3、后端服务分布式部署后,进程内登录模块,服务端session不适用了,出现了分布式登录,即单点登录 4、分布式存储HDFS

    2.2 从锁到分布式锁(为什么redis需要分布式锁)

    单个Java进程内的锁: 问题:在Java并发多线程环境下,由于上下文的切换,数据可能出现不一致的情况或者数据被污染。 期待:当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。 实现两种方式:Java中实现锁有两种方式,阻塞锁(synchronized lock)或 CAS硬件层面锁。

    进程内的多线程锁 互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。 临界值:让多线程串行话去访问资源 事件通知:通过事件的通知去保证大家都有序访问共享资源 信号量:多个任务同时访问,同时限制数量,比如发令枪CDL(即CountDownLatch),Semaphore等

    问题:为什么Redis是单线程,但是需要分布式锁? 标准答案:Redis是单线程,所以单个redis内部不需要进程内锁机制,这是可以确定的; 如果后端采用单体架构,一个Redis对应一个服务,不需要在redis中实现分布式锁; 如果后端采用微服务架构,一个Redis对应多个SOA服务,需要在redis中实现分布式锁。 解释:如果后端采用微服务架构,不使用任何分布式锁机制(mysql、zookeeper、redis) 电商网站中的秒杀,拿到库存判断,然后在减库存,双11之前,为了减少DB的压力,挡住第一批最大秒杀,把库存预热到了KV,现在KV的库存是1。服务A去Redis查询到库存发现是1,那说明我能抢到这个商品对不对,那我就准备减一了,但是还没减。同时服务B也去拿发现也是1,那我也抢到了呀,那我也减。C同理。等所有的服务都判断完了,你发现诶,怎么变成-2了,超卖了呀,这下完了。

    2.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建

    zookeeper开发中的使用场景/zookeeper有什么用: 开发用途1:统一配置管理,服务注册与订阅(共用节点,类似于eureka,作为注册中心) 开发用途1:统一配置管理,数据订阅与发布(使用 watcher 实现) 开发用途2:统一命名管理,服务命名(使用 znode特性 实现) 开发用途3:分布式锁(使用 临时顺序节点 实现,三种分布式锁:mysql zookeeper ) 开发用途4:集群管理,分布式通知(使用 监听znode 实现)

    金手指:zookeeper存储 问题1:zookeeper是什么? 回答1:zookeeper是个数据库,文件存储系统,并且有监听通知机制(观察者模式)zookeeper作为一个类Unix文件系统,zookeeper通过节点来存储数据。 问题2:接上面,zookeeper节点? 回答2:zk的节点类型有4大类:持久化节点(zk断开节点还在)、持久化顺序编号目录节点、临时目录节点(客户端断开后节点就删除了)、临时目录编号目录节点。 注意:节点名称都是唯一的,这是zookeeper实现分布式锁互斥的基石。

    2.4 zookeeper四种节点

    使用层面:zookeeper节点怎么创建(linux操作)? 在Zookeeper的数据模型中,Znode分为四种:持久(persistent), 临时(ephemeral),持久序列(persistent_sequential), and 临时序列(ephemeral_sequential)。 从zkCli创建zookeeper znode的创建语法如下 create [-s] [-e] path data // path 表示新节点的路径 data表示节点名 -s 表示有序 -e 表示临时 create /test laogong // 创建永久节点 create -e /test laogong // 创建临时节点 create -s /test // 创建顺序节点 create -e -s /test // 创建临时顺序节点 临时节点与永久节点:退出后,重新连接,上一次创建的所有临时节点都没了,但是永久节点还有。

    1、持久节点 持久模式的znode,即使创建该znode的client断开了连接,znode依然存在。当重启zookeeper之后,持久模式的znode也会重新被加载到内存中; 持久节点的znode可以有child node;

    2、临时节点 临时模式的znode,其生命周期与创建其的client相同,当创建其的client与zookeeper服务断开连接时,该临时节点就自动删除。 临时节点znode不能有child node

    3、持久有序节点 使用持久序列模式创建一个znode时,znode会自动增加一个编号; 持久模式的znode可以有child node;

    4、临时有序节点 在临时模式的基础上,增加了一个自动编号的功能。 临时znode不能有child node,一般当做其他Node的子Node。

    小结1(使用层面:创建节点):create [-s] [-e] path data // path 表示新节点的路径 data表示节点名 -s 表示有序 -e 表示临时 小结2(使用层面:子节点):持久节点的znode可以有child node,临时节点的znode可以有child node

    三、进程内无锁–进程内加锁–zookeeper分布式锁–zookeeper分布式锁

    3.1 进程内不是锁,造成线程安全问题

    public class Test implements Runnable { static int inventory = 1; // 要在main函数中用,设置为static private static final int NUM = 10; // 要在main函数中用,设置为static private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static public static void main(String[] args) { Test test = new Test(); // 10个线程都是用同一个test对象 for (int i = 1; i <= NUM; i++) { new Thread(test).start(); // 新建线程并启动 countDownLatch.countDown(); // 减1 } } @Override public void run() { // 线程运行的具体逻辑在run()里面 try { countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞 第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动 if (inventory > 0) { // 就是inventory ==1 可以进入 Thread.sleep(1); // 第二,为什么加上Thread.sleep()if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显 inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显 } System.out.println(inventory); } catch (Exception e) { e.printStackTrace(); } } } 运行结果: -1 -1 -2 -3 -3 -3 -3 -3 -3 -3

    解释代码: 第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动 第二,为什么加上Thread.sleep()? 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显

    3.2 进程内使用锁(synchronized / lock)

    public class Test1 implements Runnable { private final Lock _lock = new ReentrantLock(); static int inventory = 1; // 要在main函数中用,设置为static private static final int NUM = 10; // 要在main函数中用,设置为static private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static public static void main(String[] args) { Test1 test1=new Test1(); for (int i = 1; i <= NUM; i++) { new Thread(test1).start(); // 新建线程并启动 第三,这个10个线程,一定要传入同一个对象test1,否则锁不起作用 countDownLatch.countDown(); // 减1 } } @Override public void run() { // 线程运行的具体逻辑在run()里面 _lock.lock(); try { countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞 if (inventory > 0) { // 就是inventory ==1 可以进入 Thread.sleep(5); // 休息5ms 让线程安全问题暴露的更明显 inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显 } System.out.println(inventory); } catch (Exception e) { e.printStackTrace(); } finally { _lock.unlock(); } // try { // countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞 // synchronized (this){ // if (inventory > 0) { // 就是inventory ==1 可以进入 // Thread.sleep(5); // 休息5ms 让线程安全问题暴露的更明显 // inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显 // } // System.out.println(inventory); // } // // } catch (Exception e) { // e.printStackTrace(); // } } }

    解释代码: 第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动 第二,为什么加上Thread.sleep()? 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显 第三,这个10个线程,一定要传入同一个对象test1,否则锁不起作用

    3.3 zookeeper分布式锁(加锁+解锁+监听)

    public class Test2 implements Runnable { private static final String IP_PORT = "127.0.0.1:2181"; private static final String Z_NODE = "/LOCK"; private static final int NUM = 10; // 要在main函数中用,设置为static private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static private static ZkClient zkClient = new ZkClient(IP_PORT); static int inventory = 1; // 要在main函数中用,设置为static private static final int NUM = 10; // 要在main函数中用,设置为static public static void main(String[] args) { Test2 test2 = new Test2(); // 10个线程都是用同一个test对象 for (int i = 1; i <= NUM; i++) { new Thread(test2).start(); // 新建线程并启动 分布式锁3:原来是一个进程中10个线程,用发令枪控制10个线程同时跑 // 现在,一个后端节点一个线程,所以10个后端节点10个线程,不需要发令枪了,它只能控制同一进程里面的线程一起跑 } } @Override public void run() { // 线程运行的具体逻辑在run()里面 try { new Test2().lock(); zkClient.createPersisent(Z_NODE); // 分布式锁1:新建一个节点 节点是唯一的,已经有了,再次新建就会报错,用新建节点这个特性来实现分布式锁,相当于synchronized | lock if (inventory > 0) { // 就是inventory ==1 可以进入 Thread.sleep(1); // 第二,为什么加上Thread.sleep()if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显 inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显 } System.out.println(inventory); return; } finally { // 分布式锁4:去掉catch块 new Test2().unlock(); // 分布式锁2:在finally块,释放节点,类似lock.unlock 即,最开始新建节点类似lock.lock() finally块释放节点类似 lock.unlock(); System.out.println("释放锁"); } } public void lock() { if (tryLock()) { // 加锁成功,直接return return; } waitForLock(); // 加锁失败,然后等待,突破发令枪阻塞再开始 lock(); // 突破发令枪阻塞,再次尝试加锁 } public void waitForLock() { // 监听 System.out.println("加锁失败"); IZkDataListener listener = new IZkDataListener() { @Override public void handleDataChange(String arg0, Object arg1) throws Exception { // 修改 } @Override public void handleDataDeleted(String arg0) throws Exception { System.out.println("监听到配置文件被删除,唤醒"); // config删除,监听打印 } }; zkClient.subscribeDataChanges(Z_NODE, listener); if (zkClient.exists(Z_NODE)) { try { countDownLatch.await(); // 发令枪阻塞,从这里开始 } catch (Exception e) { e.printStackTrace(); } } zkClient.unsubscribeDataChanges(Z_NODE, listener); } }

    3.4 zookeeper分布式锁(加锁+解锁+监听+解决羊群效应)

    public class Test3 implements Runnable { private static final String IP_PORT = "127.0.0.1:2181"; private static final String Z_NODE = "/LOCK"; private static final int NUM = 10; // 要在main函数中用,设置为static private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static private static ZkClient zkClient = new ZkClient(IP_PORT); static int inventory = 1; // 要在main函数中用,设置为static private static final int NUM = 10; // 要在main函数中用,设置为static public static void main(String[] args) { Test3 test3 = new Test3(); // 10个线程都是用同一个test对象 for (int i = 1; i <= NUM; i++) { new Thread(test3).start(); // 新建线程并启动 分布式锁3:原来是一个进程中10个线程,用发令枪控制10个线程同时跑 // 现在,一个后端节点一个线程,所以10个后端节点10个线程,不需要发令枪了,它只能控制同一进程里面的线程一起跑 } } @Override public void run() { // 线程运行的具体逻辑在run()里面 try { new Test2().lock(); zkClient.createPersisent(Z_NODE); // 分布式锁1:新建一个节点 节点是唯一的,已经有了,再次新建就会报错,用新建节点这个特性来实现分布式锁,相当于synchronized | lock if (inventory > 0) { // 就是inventory ==1 可以进入 Thread.sleep(1); // 第二,为什么加上Thread.sleep()if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显 inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显 } System.out.println(inventory); return; } finally { // 分布式锁4:去掉catch块 new Test2().unlock(); // 分布式锁2:在finally块,释放节点,类似lock.unlock 即,最开始新建节点类似lock.lock() finally块释放节点类似 lock.unlock(); System.out.println("释放锁"); } } public void lock() { if (tryLock()) { // 加锁成功,直接return System.out.println("获得锁"); } else { waitForLock(); // 加锁失败,然后等待,突破发令枪阻塞再开始 lock(); // 突破发令枪阻塞,再次尝试加锁 } } public synchronized boolean tryLock() { if (StringUtils.isBlank(path)) path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock"); List<String> children = zkClient.getClildren(Z_NODE); Collections.sort(children); if (path.equals(Z_NODE + "/" + children.get(0))){ System.out.println("I am true"); return true; }else { int i=Collections.binarySearch(children,path.substring(Z_NODE.length() + 1)); beforePath= Z_NODE +"/"+children.get(i-1); } return false; } public void waitForLock() { System.out.println("加锁失败"); IZkDataListener listener = new IZkDataListener() { @Override public void handleDataChange(String arg0, Object arg1) throws Exception { // 修改 } @Override public void handleDataDeleted(String arg0) throws Exception { System.out.println("监听到配置文件被删除,唤醒"); // config删除,监听打印 } }; zkClient.subscribeDataChanges(beforePath, listener); if (zkClient.exists(Z_NODE)) { try { countDownLatch.await(); // 发令枪阻塞,从这里开始 } catch (Exception e) { e.printStackTrace(); } } zkClient.unsubscribeDataChanges(beforePath, listener); } }

    四、面试金手指

    4.1 分布式 + 从锁到分布式锁 + zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建

    4.1.1 分布式技术四个分支

    分布式相关技术:分布式部署导致分布式锁、分布式事务、分布式登录(单点登录)、分布式存储HDFS四个技术的出现 1、后端服务分布式部署后,进程内Java并发多线程锁(阻塞锁+CAS)不适用了,出现了分布式锁(三种实现方式:直接在Mysql表上实现分布式锁、引入一个中间件zookeeper实现各服务之间分布式锁、引入一个中间件redis实现各服务之间分布式锁) 2、mysql分布式部署后,事务不适用了,出现了分布式事务(七种实现方式:两阶段、三阶段、XA、最大努力、ebay本地消息表、TCC编程模式、半消息/最终一致性) 3、后端服务分布式部署后,进程内登录模块,服务端session不适用了,出现了分布式登录,即单点登录 4、分布式存储HDFS

    4.1.2 从锁到分布式锁(为什么redis需要分布式锁)

    单个Java进程内的锁: 问题:在Java并发多线程环境下,由于上下文的切换,数据可能出现不一致的情况或者数据被污染。 期待:当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。 实现两种方式:Java中实现锁有两种方式,阻塞锁(synchronized lock)或 CAS硬件层面锁。

    进程内的多线程锁 互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。 临界值:让多线程串行话去访问资源 事件通知:通过事件的通知去保证大家都有序访问共享资源 信号量:多个任务同时访问,同时限制数量,比如发令枪CDL(即CountDownLatch),Semaphore等

    问题:为什么Redis是单线程,但是需要分布式锁? 标准答案:Redis是单线程,所以单个redis内部不需要进程内锁机制,这是可以确定的; 如果后端采用单体架构,一个Redis对应一个服务,不需要在redis中实现分布式锁; 如果后端采用微服务架构,一个Redis对应多个SOA服务,需要在redis中实现分布式锁。 解释:如果后端采用微服务架构,不使用任何分布式锁机制(mysql、zookeeper、redis) 电商网站中的秒杀,拿到库存判断,然后在减库存,双11之前,为了减少DB的压力,挡住第一批最大秒杀,把库存预热到了KV,现在KV的库存是1。服务A去Redis查询到库存发现是1,那说明我能抢到这个商品对不对,那我就准备减一了,但是还没减。同时服务B也去拿发现也是1,那我也抢到了呀,那我也减。C同理。等所有的服务都判断完了,你发现诶,怎么变成-2了,超卖了呀,这下完了。

    4.1.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建

    zookeeper开发中的使用场景/zookeeper有什么用: 开发用途1:服务注册与订阅(共用节点,类似于eureka,作为注册中心) 开发用途2:分布式通知(使用 监听znode 实现) 开发用途3:服务命名(使用 znode特性 实现) 开发用途4:数据订阅、发布(使用 watcher 实现) 开发用途5:分布式锁(使用 临时顺序节点 实现,三种分布式锁:mysql zookeeper )

    金手指:zookeeper存储 问题1:zookeeper是什么? 回答1:zookeeper是个数据库,文件存储系统,并且有监听通知机制(观察者模式) 问题2:接上面,zookeeper作为一个类Unix文件系统,它是如何存储数据的? 回答2:zookeeper通过节点来存储数据。 问题3:接上面,zookeeper节点? 回答3:zk的节点类型有4大类:持久化节点(zk断开节点还在)、持久化顺序编号目录节点、临时目录节点(客户端断开后节点就删除了)、临时目录编号目录节点。 注意:节点名称都是唯一的,这是zookeeper实现分布式锁互斥的基石。

    使用层面:zookeeper节点怎么创建(linux操作)? create /test laogong // 创建永久节点 create -e /test laogong // 创建临时节点 create -s /test // 创建顺序节点 create -e -s /test // 创建临时顺序节点 临时节点与永久节点:退出后,重新连接,上一次创建的所有临时节点都没了,但是永久节点还有。

    4.2 zookeeper是如何基于节点去实现各种分布式锁的?

    总问题:zookeeper是如何基于节点去实现各种分布式锁的? 问题1:节点,zookeeper如何实现分布式锁? 回答1:和lock一样的,在子节点逻辑之前创建节点,在finally块中释放节点,两行代码实现分布式锁。 解释1: (1)在zookeeper中,节点名为唯一的,给定节点名创建一个后就不能再创建一个,会报错,创建指定节点名的节点,类似于获得到了锁; (2)与lock不同的是,lock是一个进程内,对所有线程可见,而节点名是对 分布式部署 中所有的节点可见; (3)正常情况下,会执行finally的语句,即表示节点执行完逻辑后,正常释放节点。

    问题1.1 :zookeeper如何实现分布式锁的互斥? 回答1.1 :zk节点有个唯一的特性,就是我们创建过这个节点了,你再创建zk是会报错的,那我们就利用一下他的唯一性去实现一下。我们全部去创建,创建成功的第一个返回true他就可以继续下面的扣减库存操作,后续的节点访问就会全部报错,扣减失败,我们把它们丢一个队列去排队。 问题1.2 :zookeeper实现的分布式锁,客户端如何释放锁? 回答1.2 :删除节点,Lock在finally里面unLock,现在我们在finally删除节点,删了再通知其他的客户端来争夺锁。 问题1.3 :怎么通知其他的客户端来争夺锁? 回答1.3 :监听节点的删除事件,知道指定节点名的节点被删除了,然后监听函数中就开始争夺 创建指定节点名节点。

    问题2:临时节点,接上面,上面说的是成功创建指定节点名的节点,在正常执行完逻辑后,正常执行完逻辑后,在finally中释放节点,但是,如果占有锁的节点宕机之后,造成的死锁的问题,如何处理? 回答2:临时节点,zookeeper有四种节点,永久节点在java客户端程序断开连接后会保留,临时节点在java客户端程序断开连接后会销毁,创建临时节点,这样,一旦java后端创建节点的SOA服务,这个客户端获取到锁之后突然挂掉(Session连接断开),这个临时节点会被销毁,其他客户端自动获取锁。

    问题3:临时顺序节点,客户端的监听机制(监听指定节点名的节点的 操作和删除)是所有服务都去监听一个节点的,节点的释放也会通知所有的服务器,如果是900个服务器呢?这对服务器是很大的一个挑战,一个释放的消息,就好像一个牧羊犬进入了羊群,大家都四散而开,随时可能干掉机器,会占用服务资源,网络带宽等等。造成羊群效应,如何处理羊群效应? 回答3:临时顺序节点,可以顺利解决这个问题。其他所有节点都去监听一个节点问题很大,但是监听自己的前一个节点,因为是顺序的,很容易找到自己的前后。因为每个节点只监听了自己的前一个节点,释放当然也是一个个释放下去,就不会出现羊群效应了。

    问题4:阻塞效果,加锁创建节点,但是你得实现一个阻塞的效果呀,那咋搞? 回答4:死循环,递归不断去尝试,直到成功,一个伪装的阻塞效果。zk通过节点排队监听的机制,实现了阻塞的原理,其实就是个递归在那无限等待最小节点释放的过程。

    问题5:可重入锁,zookeeper使用指定节点名的唯一性,如何实现分布式锁的可重入? 回答5:基于zookeeper的分布式锁实现可重入锁,可以带上线程信息,或者机器信息这样的唯一标识(只要是可以唯一表时间客户端连接的就好),获取的时候判断一下,如果是以获取锁的客户端,就不用再获取了,如果不是以获取锁的客户端,还需要重新获取。

    问题6:高可用,zookeeper集群实现的分布式锁,是否高可用? 回答6:zookeeper是一个分布式集群架构的,是高可用的,对于写操作,follower ACK只要半数以上,就成功了。

    问题7:用zookeeper来实现分布式锁的缺点? 回答7: 缺点1:zookeeper性能上可能并没有缓存服务redis那么高(创建和删除节点,比较重)。 解释:因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。这是非常重量的,ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。 缺点2:使用Zookeeper实现分布式锁,可能带来并发问题,只是并不常见而已。 解释:由于网络抖动,客户端可zookeeper集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题了,这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。

    五、小结

    zookeeper分布式锁,完成了。

    天天打码,天天进步!!!

    Processed: 0.019, SQL: 8