【分布式锁第四篇 基于zookeeper的分布式锁】

背景

ZooKeeper是一个高可用的分布式协调服务,由雅虎创建,是Google Chubby的开源实现。

zookeeper重要的3个特征是:

  • zab协议:通过zab协议保证数据一致性,zookeeper集群部署保证可用性
  • node存储模型:node存储在内存中,提高了数据操作性能
  • watcher机制:使用watcher机制,实现了通知机制(比如加锁成功的client释放锁时可以通知到其他client)

zookeeper node模型支持临时节点特性,即client写入的数据时临时数据,当客户端宕机时临时数据会被删除,这样就不需要给锁增加超时释放机制了。当针对同一个path并发多个创建请求时,只有一个client能创建成功,这个特性用来实现分布式锁。注意:如果client端没有宕机,由于网络原因导致zookeeper服务与client心跳失败,那么zookeeper也会把临时数据给删除掉的,这时如果client还在操作共享数据,是有一定风险的。

原理

我们一起来看看,多客户端获取及释放zk分布式锁的整个流程及背后的原理。

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

image.png

参见上图。zk里有一把锁,这个锁就是zk上的一个节点。然后呢,两个客户端都要来获取这个锁,具体是怎么来获取呢?

咱们就假设客户端A抢先一步,对zk发起了加分布式锁的请求,这个加锁请求是用到了zk中的一个特殊的概念,叫做临时顺序节点。简单来说,就是直接在"my_lock"这个锁节点下,创建一个顺序节点,这个顺序节点有zk内部自行维护的一个节点序号。

比如说,第一个客户端来搞一个顺序节点,zk内部会给起个名字叫做:xxx-000001。然后第二个客户端来搞一个顺序节点,zk可能会起个名字叫做:xxx-000002。大家注意一下,最后一个数字都是依次递增的,从1开始逐次递增。zk会维护这个顺序。所以这个时候,假如说客户端A先发起请求,就会搞出来一个顺序节点,大家看下面的图,Curator框架大概会弄成如下的样子:


image.png

客户端A发起一个加锁请求,先会在你要加锁的node下搞一个临时顺序节点,这一大坨长长的名字都是Curator框架自己生成出来的。然后,那个最后一个数字是"1"。大家注意一下,因为客户端A是第一个发起请求的,所以给他搞出来的顺序节点的序号是"1"。接着客户端A创建完一个顺序节点。还没完,他会查一下"my_lock"这个锁节点下的所有子节点,并且这些子节点是按照序号排序的,这个时候他大概会拿到这么一个集合:

[
"_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000001"
]

接着客户端A会走一个关键性的判断,就是说:唉!兄弟,这个集合里,我创建的那个顺序节点,是不是排在第一个啊?如果是的话,那我就可以加锁了啊!因为明明我就是第一个来创建顺序节点的人,所以我就是第一个尝试加分布式锁的人啊!

bingo!加锁成功!大家看下面的图,再来直观的感受一下整个过程。


image.png

接着假如说,客户端A都加完锁了,客户端B过来想要加锁了,这个时候他会干一样的事儿:先是在"my_lock"这个锁节点下创建一个临时顺序节点,此时名字会变成类似于:

"_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000002"

大家看看下面的图:


image.png

客户端B因为是第二个来创建顺序节点的,所以zk内部会维护序号为"2"。

接着客户端B会走加锁判断逻辑,查询"my_lock"锁节点下的所有子节点,按序号顺序排列,此时他看到的类似于:

[
"_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000001",
"_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000002"
]

同时检查自己创建的顺序节点,是不是集合中的第一个?明显不是啊,此时第一个是客户端A创建的那个顺序节点,序号为"01"的那个。所以加锁失败!加锁失败了以后,客户端B就会通过ZK的API对他的顺序节点的上一个顺序节点加一个监听器。zk天然就可以实现对某个节点的监听。客户端B的顺序节点是:

_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000002

他的上一个顺序节点,不就是下面这个吗?

_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000001

即客户端A创建的那个顺序节点!

所以,客户端B会对:

_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000001

这个节点加一个监听器,监听这个节点是否被删除等变化!大家看下面的图。


image.png

接着,客户端A加锁之后,可能处理了一些代码逻辑,然后就会释放锁。那么,释放锁是个什么过程呢?

其实很简单,就是把自己在zk里创建的那个顺序节点,也就是:

_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000001

这个节点给删除。

删除了那个节点之后,zk会负责通知监听这个节点的监听器,也就是客户端B之前加的那个监听器,说:兄弟,你监听的那个节点被删除了,有人释放了锁。


image.png

此时客户端B的监听器感知到了上一个顺序节点被删除,也就是排在他之前的某个客户端释放了锁。

此时,就会通知客户端B重新尝试去获取锁,也就是获取"my_lock"节点下的子节点集合,此时为:

_c_0abad917-53a6-4ed9-ac96-bfac3327be0d-lock-0000000002

集合里此时只有客户端B创建的唯一的一个顺序节点了!

然后呢,客户端B判断自己居然是集合中的第一个顺序节点,bingo!可以加锁了!直接完成加锁,运行后续的业务代码即可,运行完了之后再次释放锁。

image.png

实战

最后,咱们来看下用Curator框架进行加锁和释放锁的一个过程:

//定义锁节点名称
InterProcessMutex lock=InterProcessMutex (client,"/locks/my_lock");
try{
     lock.acquire();//加锁

    //业务逻辑代码
    // ...
}catch(Exception e){
    lock.release();//释放锁
}

使用curator进行分布式加锁示例如下:

<!--引入依赖-->
<!--对zookeeper的底层api的一些封装-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>

<!--封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>

代码示例:

public class CuratorTest {
    private static final String LOCK_PATH = "/curator_recipes_lock_path";

    private static final String ZK_ADDRESS = "192.168.193.128:2181";

    public static void main(String[] args) throws Exception {

        CuratorFramework client = CuratorFrameworkFactory.builder().connectString(ZK_ADDRESS)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();
        client.start();
        
        // 创建ZK分布式锁
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);

        Runnable task = () -> {
            try {
                // 获取锁
                lock.acquire();
                try {
                    // 处理业务逻辑
                    System.out.println("zookeeper acquire success: " + Thread.currentThread().getName());
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.release();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        };

        // 创建10线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);
        // 模拟1000次的锁争抢
        for (int i = 0; i < 1000; i++) {
            executor.execute(task);
        }
        LockSupport.park();
    }
}

总结

原理小结

其实如果有客户端C、客户端D等N个客户端争抢一个zk分布式锁,原理都是类似的。大家都是上来直接创建一个锁节点下的一个接一个的临时顺序节点如果自己不是第一个节点,就对自己上一个节点加监听器只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。而且用临时顺序节点的另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系,zk感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。

ZK分布式锁的实现原理图

使用建议

基于zookeeper实现分布式锁,相对于基于redis和DB的实现来说,使用上更容易,效率与稳定性较好。curator封装了对zookeeper的API操作,同时也封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等。

如果你要手动实现一套那个代码的话。还是有点麻烦的,要考虑到各种细节,异常处理等等。一般除了大公司是自行封装分布式锁框架之外,建议大家Curator这个开源框架对于分布式锁实现,这是一个比较快捷省事儿的方式。

参考资料

  1. 分布式锁设计与实现
  2. 七张图彻底讲清楚ZooKeeper分布式锁的实现原理
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容