ZooKeeper典型应用——分布式锁

版权声明:坚持原创,坚持深入原理,未经博主允许不得转载!! https://blog.csdn.net/lemon89/article/details/76268820

Zookeeper是一个典型的解决分布式数据一致性问题的框架,我们来看看如何使用Zookeeper实现分布式锁。

如果对JDK锁核心实现不理解的,推荐阅读
java并发编程——读写锁ReentrantReadWriteLock
java并发编程——ReentrantLock源码(重入锁、公平锁、非公平锁)
图解java并发(上)

ZooKeeper分布式锁

分布式锁是用来控制分布式系统各个节点同步访问共享节点的一种锁机制。

在ZK中通过使用一个ZNode节点来表示一个排他锁如:/zs/zookeeperLock/lock)。

排他锁

获取锁

多个客户端并发的在ZooKeeper上创建一个临时节点(临时节点保证获取到锁的客户端挂掉后可以自动释放),如/zs/zookeeperLock/lock。节点的唯一性保证只有一个客户端可以成功创建(成功获取锁)。

其他没有成功创建节点的客户端,在/zs/zookeeperLock/节点上注册一个子节点变更事件Watcher监听,以便异步即使的监听锁释放,并再次尝试获取锁。

释放锁

  • 获取锁的进程挂掉,自动释放这个锁(临时节点)。
  • 当锁使用完,主动删除节点,释放锁。

共享锁

其实就是一个读写锁,它允许多个读操作同时共享锁。对于写操作需要获取写锁,获取写锁的前提是不存在读锁或者写锁被其他进程获取,也就是锁写锁是排他的。

获取锁

通过创建类似/zs/zookeeperLock/sharedLock/ip-type-id 的临时顺序节点来代表读读写锁
其中type有两种枚举值:R、W。分别代表读锁(共享锁)、写锁。

如果是获取读锁(一般是读请求)会在ZooKeeper上创建如下类似节点:
/zs/zookeeperLock/sharedLock/10.0.10.1-R-0000000001。

如果是获取写锁(排他锁)会在ZooKeeper上创建如下类似节点:
/zs/zookeeperLock/sharedLock/10.0.10.1-W-0000000001。

具体获取锁步骤

扫描二维码关注公众号,回复: 3247808 查看本文章

1.创建当前进程的子节点: /zs/zookeeperLock/sharedLock节点下创建属于自己的临时子节点

2.获取子节点列表:1创建完成后,获取/zs/zookeeperLock/sharedLock节点下所有的子节点列表

3.对于获取读锁的请求:如果不存在比当前进程节点的序号小的W类型节点(此时没有写锁被其他进程持有),或者比当前进程节点序号小的节点都是R类型节点(此时只有读锁被其他进程持有,可共享)——那么代表当前进程获取读成功

对于获取写锁的请求:如果不存在比当前进程节点的序号小的任何类型节点(此时没有读锁或者写锁被其他进程持有)——那么代表当前进程获取写成功

4.获取失败后注册Watcher监听-等待锁
获取失败后在/zs/zookeeperLock注册Watcher监听子节点的变更事件,如果子节点列表发生变化再次进入2中尝试获取锁。

可以看到通过创建有序临时节点实现了公平的读写锁

释放锁

与排他锁的释放一致。不赘述。

共享锁存在的问题?

上述共享锁在处理读写锁排序时,明显有以下两个问题:

无效的事件知
每次子节点列表变更所有进程都会收到通知。如果/zs/zookeeperLock/sharedLock 有10个子节点,那么可有可能最后一个节点对应的进程会收到9次监听事件通知,重复尝试9次获取锁后才能成功,这前9次获取完全是无效的。浪费网络、cpu等资源。

更严重的是,如果同一时间多个子节点变更,会导致ZK短时间内向客户端发送大量通知,造成较大的性能影响和网络冲击——羊群效应

那么能不能减少不必要的通知呢?其实每个节点只需要关注它的上一个节点即可,而不需要关注全局子列表的变更。
熟悉JDK所实现的话,就可以想到AQS中的队列节点只需要关注前驱节点。

通常集群规模较大(大于10),应该采用这种实现。

具体实现如下(区别只在于第四步):

1.创建当前进程的子节点: /zs/zookeeperLock/sharedLock节点下创建属于自己的临时子节点

2.获取子节点列表:1创建完成后,获取/zs/zookeeperLock/sharedLock节点下所有的子节点列表

3.对于获取读锁的请求:如果不存在比当前进程节点的序号小的W类型节点(此时没有写锁被其他进程持有),或者比当前进程节点序号小的节点都是R类型节点(此时只有读锁被其他进程持有,可共享)——那么代表当前进程获取读成功

对于获取写锁的请求:如果不存在比当前进程节点的序号小的任何类型节点(此时没有读锁或者写锁被其他进程持有)——那么代表当前进程获取写成功

4.获取失败后注册Watcher监听-等待锁
对于读请求:向比自己节点序号小的最后一个写请求的节点注册一个Watcher监听。收到通知则进入2中尝试获取锁。
对于写请求:向比自己节点序号小的最后一个节点(不用区分读写类型,也就是自己的前驱节点)注册一个Watcher监听。收到通知则进入2中尝试获取锁。

可以看到区别就是第四步注册的事件不同,只针对自己关注的事件注册。

代码实现

以下使用Curator(ZooKeeper的客户端API封装)来实现:

public class distributedLock {

    static String lock_path = "/curator_recipes_lock_path";
    static CuratorFramework zkClient = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build();

    public static void main(String[] args) {

        zkClient.start();
        final InterProcessMutex lock = new InterProcessMutex(zkClient, lock_path);
        final CountDownLatch down = new CountDownLatch(1);

        for (int i = 0; i < 60; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        down.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        lock.acquire();

                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
                        System.err.println("Order Number :" + sdf.format(new Date()));

                        lock.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        down.countDown();
    }
}

出了ZooKeeper还有Redis也可以实现分布式锁,可参考:
Redis实现分布式锁
聊一聊分布式锁的设计

当然还有数据库、ZooKeeper都可以实现分布式锁:
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

猜你喜欢

转载自blog.csdn.net/lemon89/article/details/76268820