2023-08-12  阅读(1)
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/187

我在分布式框架之高性能:Redis分布式锁一章中,介绍过Redis分布式锁。事实上,生产环境中,Zookeeper分布式锁更加成熟,在工业运用中也更多。

本章,我将基于比较常用的Curator开源框架,来聊一聊Curator对ZooKeeper(以下简称ZK)分布式锁的实现。

Curator可以看成是Zookeeper Client,类似于Jedis、Redisson之于Redis,读者可以从这里获取到Curator这个ZK客户端的更多资料:http://curator.apache.org

一、基本原理

首先大家看看下面的图,如果现在有两个客户端要一起争抢ZK上的一把分布式锁,会是个什么场景?

202308122223346271.png

上图中,ZK里有一把锁“my_lock”,这个锁其实就是ZK的一个znode。然后两个客户端都要来获取这个锁,具体是怎么来获取呢?

1.1 创建临时顺序节点

我们假设客户端A抢先一步,对ZK发起了加分布式锁的请求,这个加锁请求其实就是在"my_lock"这个znode下,创建一个 临时顺序节点 ,这个顺序节点有ZK内部自行维护的一个节点序号。

比如说,第一个客户端来创建一个顺序节点,zk内部会给它起个名字叫做:xxx-000001。然后第二个客户端来搞创建一个同名节点,ZK会起另一个名字叫做:xxx-000002。大家注意一下, 最后一个数字都是依次递增的 ,从1开始逐次递增,ZK会维护这个顺序。

大家看下面的图,Curator框架大概会弄成如下的样子:

202308122223355502.png

也就是说,客户端A发起一个加锁请求,则会在要加锁的node下搞一个临时顺序节点,这一大坨长长的名字都是Curator框架自己生成出来的。注意一下,因为客户端A是第一个发起请求的,所以顺序节点的序号是"1"。

客户端A创建完一个顺序节点后,他会查一下"my_lock"这个znode下的所有子节点,他大概会拿到这么一个集合:

    [
        "_c_0abad917-53a6-ab12-872a-bfac2d12a20a-lock-0000000001"
    ]

然后,客户端A判断自己创建的那个顺序节点是不是排在第一个的,如果是就加锁成功了:

202308122223367723.png

1.2 创建监听器

接着,客户端B过来想要加锁,这个时候他也会干一样的事儿:在"my_lock"这个znode下创建一个临时顺序节点:

202308122223386884.png

因为客户端B是第二个创建临时顺序节点的,所以ZK内部会维护序号为"2"。接着客户端B同样会走加锁判断逻辑,查询"my_lock"这个znode下的所有子节点,按序号顺序排列,此时他看到的类似于:

    [
       "_c_0abad917-53a6-ab12-872a-bfac2d12a20a-lock-0000000001",
       "_c_0abad917-18a6-ab12-872a-dac2d12a201a-lock-0000000002"
    ]

客户端B还想自己创建的顺序节点不是第一个,所以加锁失败!加锁失败以后,客户端B就会通过ZK的API, 对他的上一个顺序节点加一个监听器 ,监听这个节点是否被删除等变化。

说了那么多,老规矩,给大家来一张图,直观的感受一下:

202308122223399175.png

1.3 删除临时顺序节点

接着,客户端A加锁成功之后,处理自身的业务逻辑,处理完后就会释放锁。释放锁其实就是把自己在ZK里创建的那个顺序节点给删除掉。删除了这个节点之后,ZK会负责通知监听该节点的监听器,也就是客户端B之前加的那个监听器:

202308122223417106.png

此时,客户端B的监听器感知到了上一个顺序节点被删除,就会通知客户端B重新尝试去获取锁。客户端B一判断,发现自己居然是集合中的第一个顺序节点,然后就可以加锁了。加锁成功后,执行自身业务逻辑,然后释放锁:

202308122223433547.png


如果有客户端C、客户端D等N个客户端争抢一个ZK分布式锁,原理都是类似的。大家都是上来直接在某个znode下的一个接一个得创建临时顺序节点:

  • 如果自己不是第一个节点,就对自己上一个节点加监听器;
  • 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。

而且用临时顺序节点的另外一个用意是:如果某个客户端创建临时顺序节点之后,自己宕机了也没关系,因为ZK感知到那个客户端宕机后,会自动删除对应的临时顺序节点,相当于自动释放了锁。

了解了Zookeeper分布式锁的基本原理,咱们来看下用Curator框架进行加锁和释放锁的一个过程:

    // 定义锁节点名称
    InterProcessMutex lock = InterProcessMutex(client, "/locks/my_lock");
    
    // 加锁
    lock.acquire();
    
    // 业务逻辑代码...
    
    // 释放锁
    lock.release();

二、重复加锁问题

当使用Zookeeper来实现分布式锁时,如果网络发生分区,可能会出现“脑裂问题”。

比如,客户端A加锁成功后,如果客户端A和Zookeeper之间网络断开,Zookeeper未收到客户端A的心跳,可能认为客户端A挂了,就会释放锁。此时,之前等待的客户端B就会获取到锁,导致客户端B和客户端A都认为自己获取到了同一把锁,就会出现问题。

解决该问题的一个基本思路就是,Zookeeper在创建“锁”时保存对应客户端的标识,这样客户端A如果挂了,客户端B尝试来获取锁时,Zookeeper就会判断客户端A的锁还没释放,就会拒绝客户端B获取锁。

三、优缺点

我们来比较下Zookeeper分布式锁和Redis分布式锁。

3.1 优点

Zookeeper分布式锁,如果客户端获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小;而Redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

另外一点,Zookeeper创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁;而Redis获取锁的那个客户端如果挂了,那么只能等待超时时间之后才能释放锁。

最后,从分布式系统的协调语义来看,ZooKeeper做分布式锁更好一些,因为Redis本身其实是缓存。

3.2 缺点

Zookeeper本身不适合大规模集群部署,其适用场景就是部署三五台机器,不是承载高并发请求的,仅仅是用作分布式系统的协调工作;而Redis本身其实是缓存,能抗高并发,在高并发场景下性能更好一些。

四、总结

本章,我介绍了Zookeeper分布式锁的基本原理,并拿它和Redis分布式锁进行了比较。一般来讲,绝大多数的公司其实很少有超高并发的业务场景,那么ZooKeeper分布式锁基本都能满足需求。对于一些大型公司的核心业务,一般也会针对实际的业务场景,对ZK分布式锁或Redis分布式锁进行定制改造,以满足自身业务需求。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文