zookeeper集群实现分布式锁

在分布式系统中,会出现不同服务访问同一资源的情况,很容易出现读写信息不一致的现象,所以需要用到分布式锁。去哪找一把稳定的、可靠,具备一定性能的锁。zookeeper就能实现,本篇就写一个小demo,依然使用前两篇的zookeeper集群,实现分布式锁。

设计思路

首先我们先解决两个问题

  1. 即时性:当一个服务释放锁之后,其他服务怎么及时发现呢,这里用到了zookeeper的watch特性。
  2. 并发压力问题:如果有很多服务同时竞争锁,不可避免对锁的服务带来很大的压力,这里用到zookeeper的序列节点,每个服务创建临时序列节点,并监听排在它前面的一个节点。

实现步骤

封装zookeeper工具及对应watch类(与上篇一致)

封装zookeeper获取工具类

public class ZookeeperUtil {
    // 地址
    private static ZooKeeper zk;
    private static String url = "192.168.137.131:2181,192.168.137.132:2181" +
             ",192.168.137.134:2181,192.168.128.150:2181/testConfig";

    private static DefaultWatch watch = new DefaultWatch();
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    public static ZooKeeper getZookeeper() {

        try {
            zk = new ZooKeeper(url,1000,watch);
            watch.setCc(countDownLatch);

            countDownLatch.await();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return zk;
    }
}

封装watch

public class DefaultWatch implements Watcher {
    CountDownLatch cc;

    public void setCc(CountDownLatch cc) {
        this.cc = cc;
    }

    @Override
    public void process(WatchedEvent watchedEvent) {

        Event.KeeperState state = watchedEvent.getState();

        switch (state) {
            case Unknown:
                break;
            case Disconnected:
                break;
            case NoSyncConnected:
                break;
            case SyncConnected:
                System.out.println("zookeeper connectioned");
                cc.countDown();
                break;
            case AuthFailed:
                break;
            case ConnectedReadOnly:
                break;
            case SaslAuthenticated:
                break;
            case Expired:
                break;
        }
    }
}
创建锁操作类
public class LockWatchCallback implements AsyncCallback.StringCallback, AsyncCallback.Children2Callback, Watcher,AsyncCallback.StatCallback {
    ZooKeeper zk;
    // 线程名字
    String threadName;
    CountDownLatch cc = new CountDownLatch(1);
    // 创建节点的名字,孩子节点中判断会用到
    String pathName;

    // 上锁
    public void tryLock() {
         try {
             // 创建临时序列节点
             zk.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "aaa");
             cc.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建节点后回调,获取其孩子节点
     * @param i
     * @param s
     * @param o
     * @param s1 创建节点的名字
     */
    @Override
    public void processResult(int i, String s, Object o, String s1) {
        if(s1 != null) {
            pathName = s1;
            System.out.println("pathName:" + pathName + threadName);
            zk.getChildren("/", false, this, "aaa");
        }

    }

    /**
     * 获取孩子节点后回调
     * @param i
     * @param s
     * @param o
     * @param list 孩子列表
     * @param stat
     */
    @Override
    public void processResult(int i, String s, Object o, List<String> list, Stat stat) {
        // 排序
        Collections.sort(list);
        int index = list.indexOf(pathName.substring(1));
        // 判断自己是否为第一个节点
        if(index == 0) {
            try {
                System.out.println("我是线程:" + threadName);
                // 别跑太快,完了后面的还没监听上
                zk.setData("/", pathName.getBytes(), -1);
                cc.countDown();

            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            zk.exists("/" + list.get(index-1), this, this, "aa");
        }
    }

    /**
     * 释放锁
     */
    public void cancelLock() {
        try {
            zk.delete(pathName, -1);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    /**
     * 监听
     * @param event
     */
    @Override
    public void process(WatchedEvent event) {

        switch (event.getType()) {
            case None:
                break;
            case NodeCreated:
                break;
            case NodeDeleted:
                // 重新注册到前一节点或获得锁
                zk.getChildren("/", false, this, "aaa");
                break;
            case NodeDataChanged:
                break;
            case NodeChildrenChanged:
                break;
        }
    }

    /**
     * exisit回调
     * @param i
     * @param s
     * @param o
     * @param stat
     */
    @Override
    public void processResult(int i, String s, Object o, Stat stat) {

    }
}
创建多个线程模拟多个服务抢锁
public class TestLock {
    ZooKeeper zk;

    @Before
    public void getZk() {
        zk = ZookeeperUtil.getZookeeper();
    }

    @After
    public void closeZk() {
        try {
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testLock() {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LockWatchCallback watchCallback = new LockWatchCallback();
                watchCallback.setZk(zk);
                watchCallback.setThreadName(Thread.currentThread().getName());
                // 上锁
                watchCallback.tryLock();
                // 执行方法
                System.out.println("i am run");
                // 释放锁
                watchCallback.cancelLock();
            }).start();
        }
        while (true) {

        }
    }

}

运行结果

可以从结果中看到:

  1. 每个线程的运行顺序和创建节点的顺序是一致的。
  2. 第一个节点会直接运行,后续节点依次监听上一个节点
zookeeper connectioned
Thread-9 /lock0000000150
Thread-5 /lock0000000151
Thread-6 /lock0000000152
Thread-4 /lock0000000153
Thread-8 /lock0000000154
Thread-2 /lock0000000155
Thread-7 /lock0000000156
Thread-0 /lock0000000157
Thread-1 /lock0000000158
Thread-3 /lock0000000159
我是线程:Thread-9
我监听了1
i am run
我监听了2
我监听了3
我监听了4
我监听了5
我监听了6
我监听了7
我监听了8
我监听了9
我是线程:Thread-5
i am run
我是线程:Thread-6
i am run
我是线程:Thread-4
i am run
我是线程:Thread-8
i am run
我是线程:Thread-2
i am run
我是线程:Thread-7
i am run
我是线程:Thread-0
i am run
我是线程:Thread-1
i am run
我是线程:Thread-3
i am run

说在最后

手敲了上述代码(能跑起来),不禁感叹基于回调和监听的响应式编程方式确实很绕,还需多练啊。这只是最基础的使用方式,算是抛砖引玉吧,更多用法就要结合具体业务了。