Zookeeper--Curator典型使用場景

Curator不僅爲開發者提供了更爲便利的API接口,而且還提供了一些典型場景的使用參考。這些使用參考都在recipes包中,讀者需要單獨依賴以下Maven依賴來獲取:

<!-- Curator工具包 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.4.2</version>
        </dependency>

事件監聽

ZooKeeper原生支持通過註冊Watcher來進行事件監聽,但是其使用並不是特別方便,需要開發人員自己反覆註冊Watcher, 比較繁瑣。Curator 引入了Cache 來實現對ZooKeeper服務端事件的監聽。Cache是Curator中對事件監聽的包裝,其對事件的監聽其實可以近似看作是一個本地緩存視圖和遠程ZooKeeper視圖的對比過程。同時Curator能夠自動爲開發人員處理反覆註冊監聽,從而大大簡化了原生API開發的繁瑣過程。

Cache分爲兩類監聽類型:節點監聽和子節點監聽

NodeCache:
NodeCache用於監聽指定Zookeeper數據結點本身的變化,其構造方法有如下兩個:

  1. public NodeCache(CuratorFramework client, String path)
  2. public NodeCache(CuratorFramework client, String path, boolean dataIsCompressed)

在這裏插入圖片描述

同時,NodeCache定義了事件處理的回調接口NodeCacheListener:

public interface NodeCacheListener {
    void nodeChanged() throws Exception;
}

當數據節點的內容發生變化的時候,就會回調該方法。

public class NodeCache_Sample {
    static String path = "/zk-book/nodecache";
    static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .sessionTimeoutMs(5000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();

    public static void main(String[] args) throws Exception {
        client.start();
        client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.EPHEMERAL)
                .forPath(path, "init".getBytes());

        final NodeCache cache = new NodeCache(client, path, false);
        cache.start(true);
        cache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                System.out.println("Node data update, new data:" + new String(cache.getCurrentData().getData()));
            }
        });
        client.setData().forPath(path, "u".getBytes());
        Thread.sleep(1000);
        client.delete().deletingChildrenIfNeeded().forPath(path);
        Thread.sleep(Integer.MAX_VALUE);
    }
}

在上面的示例程序中,首先構造了一個NodeCache實例,然後調用start方法,該方法有個boolean類型的參數,默認是false,如果設置爲true,那麼NodeCache在第一次啓動的時候就會立刻從ZooKeeper上讀取對應節點的數據內容,並保存在Cache中。

NodeCache不僅可以用於監聽數據節點的內容變更,也能監聽指定節點是否存在。如果原本節點不存在,那麼Cache就會在節點被創建後觸發NodeCacheListener。但是,如果該數據節點被刪除,那麼Curator 就無法觸發NodeCacheListener了。

PathChildrenCache:

PathChildrenCache用於監聽指定Zookeeper數據結點的子節點變化情況。構造方法如下:

  1. public PathChildrenCache(CuratorFramework client, String path, boolean cacheData);
  2. public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, ThreadFactory threadFactory);
  3. public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, ThreadFactroy threadFactory);
  4. public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, final ExecutorService executorService);
  5. public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean datalsCompressed, final CloseableExecutorService executorService);

在這裏插入圖片描述

PathChildrenCache定義了事件處理的回調接口PathChildrenCacheListener:

public interface PathChildrenCacheListener {
    void childEvent(CuratorFramework var1, PathChildrenCacheEvent var2) throws Exception;
}

當指定節點的子節點發生變化時,就會回調該方法。PathChildrenCacheEvent類中定義了所有的事件類型,主要包括

  • 新增子節點(CHILD_ ADDED).
  • 子節點數據變更(CHILD_ UPDATED)
  • 子節點刪除(CHILD_ REMOVED)
public class PathChildrenCache_Sample {
    static String path = "/zk-book";
    static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .sessionTimeoutMs(5000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();

    public static void main(String[] args) throws Exception {
        client.start();
        PathChildrenCache cache = new PathChildrenCache(client, path,true);
        cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
        cache.getListenable().addListener(new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                switch (pathChildrenCacheEvent.getType()) {
                    case CHILD_ADDED:
                        System.out.println("CHILD_ADDED," + pathChildrenCacheEvent.getData().getPath());
                        break;
                    case CHILD_UPDATED:
                        System.out.println("CHILD_UPDATED," + pathChildrenCacheEvent.getData().getPath());
                        break;
                    case CHILD_REMOVED:
                        System.out.println("CHILD_REMOVED," + pathChildrenCacheEvent.getData().getPath());
                        break;
                    default:
                        break;
                }
            }
        });
        client.create().withMode(CreateMode.PERSISTENT).forPath(path);
        Thread.sleep(1000);

        client.create().withMode(CreateMode.PERSISTENT).forPath(path + "/c1");
        Thread.sleep(1000);
        client.delete().forPath(path + "/c1");
        Thread.sleep(1000);
        client.delete().forPath(path);
        Thread.sleep(Integer.MAX_VALUE);
    }
}
CHILD_ADDED,/zk-book/c1
CHILD_REMOVED,/zk-book/c1

在上面這個示例程序中,對/zk-book 節點進行了子節點變更事件的監聽,一旦該節點新增/刪除子節點,或者子節點數據發生變更,就會回調PathChildren CacheListener,並根據對應的事件類型進行相關的處理。同時,我們也看到,對於節點/zk-book本身的變更,並沒有通知到客戶端。

另外,和其他ZooKeeper客戶端產品一樣,Curator 也無法對二級子節點進行事件監聽。也就是說,如果使用PathChildrenCache 對/zk-book 進行監聽,那麼當/zk-book/c1/c2節點被創建或刪除的時候,是無法觸發子節點變更事件的。

Master選舉

在Zookeeper中可以比較方便地實現Master選舉的功能,其大體思路非常簡單:

選擇一個根節點,例如/master_select, 多臺機器同時向該節點創建一個子節點/master_ selectlock,利用ZooKeeper的特性,最終只有一臺機器能夠創建成功,成功的那臺機器就作爲Master。

Curator也是基於這個思路,但是它將節點創建、事件監聽和自動選舉過程進行了封裝,開發人員只需要調用簡單的API即可實現Master選舉。

//使用Curator實現分佈式Master選舉
public class Recipes_MasterSelect {
    static String master_path = "/curator_recipes_master_path";
    static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();

    public static void main(String[] args) throws InterruptedException {
        client.start();
        LeaderSelector selector = new LeaderSelector(client,
                master_path,
                new LeaderSelectorListenerAdapter() {
                    @Override
                    public void takeLeadership(CuratorFramework curatorFramework) throws Exception {
                        System.out.println("稱爲Master角色");
                        Thread.sleep(3000);
                        System.out.println("完成Master操作,釋放Master權利");
                    }
                });
        selector.autoRequeue();
        selector.start();
        Thread.sleep(Integer.MAX_VALUE);
    }

}

在上面這個示例程序中,可以看到主要是創建了一個LeaderSelector實例,該實例,負責封裝所有和Master選舉相關的邏輯,包括所有和ZooKeeper服務器的交互過程。其.中master_path代表了一個Master選舉的根節點,表明本次Master選舉都是在該節點下進行的。

在創建LeaderSelector 實例的時候,還會傳入一個監聽器: Leade rSelectorListenerAdapter。這需要開發人員自行實現,Curator 會在成功獲取Master權利的時候回調該監聽器,其定義如下。

public interface LeaderSelectorListener extends ConnectionStateListener {
    void takeLeadership(CuratorFramework var1) throws Exception;
}

//實現類
public abstract class LeaderSelectorListenerAdapter implements LeaderSelectorListener {
    public LeaderSelectorListenerAdapter() {
    }

    public void stateChanged(CuratorFramework client, ConnectionState newState) {
        if (newState == ConnectionState.SUSPENDED || newState == ConnectionState.LOST) {
            throw new CancelLeadershipException();
        }
    }
}

LeaderSelectorListener接口中最重要的方法就是takeLeadership方法,Curator會在競爭到Master後自動調用該方法,開發者可以在這個方法中實現自己的業務邏輯。需要注意的一點是,一旦執行完takeLeadership方法,Curator就會立即釋放Master權利,然後重新開始新一輪的Master選舉。

在示例程序中,通過sleep來簡單地模擬業務邏輯的執行,同時運行兩個應用程序後,仔細觀察控制檯輸出,可以發現,當一個應用程序完成Master邏輯後,另一個應用程序的takeLeadership方法纔會被調用。這也就說明,當一個應用實例成爲Master後,其他應用實例會進入等待,直到當前Master掛了或退出後纔會開始選舉新的Master。

同時,可以在Zookeeper上的/curator_recipes_master_path節點下,會不斷有子節點被創建出來:
在這裏插入圖片描述

分佈式鎖

在分佈式環境中,爲了保證數據的一致性,經常在程序的某個運行點(例如,減庫存操作或流水號生成等)需要進行同步控制。以一個“流水號生成”的場景爲例,普通的後臺應用通常都是使用時間戳方式來生成流水號,但是在用戶量非常大的情況下,可能會出現併發問題。

示例:典型的併發問題

public class Recipes_NoLock {
    public static void main(String[] args) {
        final CountDownLatch down = new CountDownLatch(1);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    down.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
                String orderNo = sdf.format(new Date());
                System.out.println("生成的訂單號是: " + orderNo);
            }).start();
        }
        down.countDown();
    }
}
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104
生成的訂單號是: 17:24:09|104

使用Curator實現分佈式鎖功能:

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

    public static void main(String[] args) {
        client.start();
        final InterProcessMutex lock = new InterProcessMutex(client, lock_path);
        final CountDownLatch downLatch = new CountDownLatch(1);
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                try {
                    downLatch.await();
                    lock.acquire();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss|SSS");
                String orderNo = sdf.format(new Date());
                System.out.println("生成的訂單號是:" + orderNo);
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            downLatch.countDown();
        }
    }
}
生成的訂單號是:20:06:27|915
生成的訂單號是:20:06:27|948
生成的訂單號是:20:06:27|971
生成的訂單號是:20:06:27|990
生成的訂單號是:20:06:28|001
生成的訂單號是:20:06:28|008
生成的訂單號是:20:06:28|018
生成的訂單號是:20:06:28|030

.上面這個示例程序就藉助Curator來實現了-一個簡單的分佈式鎖。其核心接口如下:

public interface InterProcessLock
- public void acquire() throws Exception;
- public void release() throws Exception;

這兩個接口分別用來實現分佈式鎖的獲取與釋放過程。

分佈式計數器

基於Zookeeper的分佈式計數器的實現思路也非常簡單:
指定一個Zookeeper數據結點作爲計數器,多個應用示例在分佈式鎖的控制下,通過更新該數據結點的內容來實現計數功能。

Curator同樣將這一系列邏輯封裝在了DistributedAtomicInteger類中,從其類名我們可以看出這是一個可以在分佈式環境中使用的原子整型,其具體使用方式如下:

//使用Curator實現分佈式計數器
public class Recipes_DistAtomicInt {
    static String distatomicint_path = "/curator_recipes_distatomicint_path";
    static CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();

    public static void main(String[] args) throws Exception {
        client.start();
        DistributedAtomicInteger atomicInteger = new DistributedAtomicInteger(client,
                distatomicint_path, new RetryNTimes(3, 1000));
        AtomicValue<Integer> rc = atomicInteger.add(8);
        System.out.println("Result: " + rc.succeeded());
    }
}

分佈式Barrier

Barrier是- -種用來控制多線程之間同步的經典方式,在JDK中也自帶了CyclicBarrier實現。

使用CyclicBarrier模擬一個賽跑比賽:

public class Recipes_CyclicBarrier {
    public static CyclicBarrier barrier = new CyclicBarrier(3);

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        executor.submit(new Thread(new Runner("1號選手")));
        executor.submit(new Thread(new Runner("2號選手")));
        executor.submit(new Thread(new Runner("3號選手")));
        executor.shutdown();
    }
}

class Runner implements Runnable {
    private String name;

    public Runner(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + "準備好了.");
        try {
            Recipes_CyclicBarrier.barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(name + "起跑!");
    }
}
1號選手準備好了.
3號選手準備好了.
2號選手準備好了.
3號選手起跑!
2號選手起跑!
1號選手起跑!

上面就是一個使用JDK自帶的CyclicBarrier實現的賽跑比賽程序,可以看到多線程在併發情況下,都會準確地等待所有線程都處於就緒狀態後纔開始同時執行其他業務邏輯。如果是在同一個JVM中的話,使用CyclicBarrier完全可以解決諸如此類的多線程同步問題。但是,如果是在分佈式環境中又該如何解決呢?Curator中提供的DistributedBarrier就是用來實現分佈式Barrier的。

//使用Curator實現分佈式Barrier
public class Recipes_Barrier {
    static String barrier_path = "/curator_recipes_barrier_path";
    static DistributedBarrier barrier;
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    CuratorFramework client = CuratorFrameworkFactory
                            .builder()
                            .connectString("127.0.0.1:2181")
                            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                            .build();
                    client.start();
                    barrier = new DistributedBarrier(client, barrier_path);
                    System.out.println(Thread.currentThread().getName() + "號barrier設置");
                    barrier.setBarrier();
                    barrier.waitOnBarrier();
                    System.err.println("啓動...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            Thread.sleep(10000);
            barrier.removeBarrier();
        }
    }
}

在上面這個實例程序中,我們模擬了5個線程,通過調用DistributedBarrier .setBarrier()方法來完成Barrier 的設置,並通過調用Dist ributedBarrier.waitOnBarrier()方法來等待Barrier 的釋放。然後在主線程中,通過調用DistributedBarrier.removeBarrier()方法來釋放Barrier, 同時觸發所有等待該Barrier的5個線程同時進行各自的業務邏輯。

和上面這種由主線程來觸發Barrier釋放不同的是,Curator還提供了另一種線程自發觸發Barrier釋放的模式,使用方式如下:

public class Recipis_Barrier2 {
    static String barrier_path = "/curator_recipes_barrier_path";

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    CuratorFramework client = CuratorFrameworkFactory.builder()
                            .connectString("127.0.0.1:2181")
                            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                            .build();
                    client.start();
                    DistributedDoubleBarrier barrier = new DistributedDoubleBarrier(client, barrier_path, 5);
                    Thread.sleep(Math.round(Math.random() * 3000));
                    System.out.println(Thread.currentThread().getName() + "號進入barrier");
                    barrier.enter();
                    System.out.println("啓動...");
                    Thread.sleep(Math.round(Math.random() * 3000));
                    barrier.leave();
                    System.out.println("退出...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
Thread-3號進入barrier
Thread-0號進入barrier
Thread-2號進入barrier
Thread-1號進入barrier
Thread-4號進入barrier
啓動...
啓動...
啓動...
啓動...
啓動...
退出...
退出...
退出...
退出...
退出...

上面這個示例程序就是一個和JDK自帶的cyclicBarrier非常類似的實現了,它們都指定了進入Barrier的成員數閾值,例如上面示例程序中的“5”。 每個Barrier 的參與者都會在調用DistributedDoubleBarrier .enter()方法之後進行等待,此時處於準備進入狀態。一旦準備進入Barrier的成員數達到5個後,所有的成員會被同時觸發進入。

之後調用DistributedDoubleBarrier .leave()方法則會再次等待,此時處於準備退出狀態。一旦準備退出Barrier的成員數達到5個後,所有的成員同樣會被同時觸發退出。因此,使用Curator的DistributedDoubleBarrier能夠很好地實現一個分佈式Barrier,並控制其同時進入和退出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章