zooker分布式锁实现

思路

  1. 使用zookeeper创建以锁资源为根节点。每次申请锁时,创建有序的临时节点。
  2. zookeeper创建有序节点时,会自动创建节点编号,所以取最小序节点为锁标志。
  3. 其他申请均轮询监听前一节点,形成单链表式数据结构,当监听到前一节点释放或销毁时,自动获得锁资源。

难点

  1. 为什么要监听前一节点状态?
    如果后续多个节点都监听最小序节点,形成单一节点被多个客户端监听,会给服务器造成很大的压力。
  2. 如何监听前一临时有序节点?
    zookeeper创建有序节点的节点值,返回的是当前节点的序号。
    获取锁资源下的所有节点并排序,使用当前节点查找在list中的下标,再根据下标值-1,即可找到前一节点。
    使用zookeeper监听节点函数,监听前一节点状态,当监听到前节点删除时,唤醒当前线程,重新竞争锁资源。

代码

public class Zk implements Lock {
    private static final String IP_PORT = "127.0.0.1:2181";
    private static final String Z_NODE = "/LOCK";

    private String beforePath;
    private String path;

    private final ZkClient zkClient = new ZkClient(IP_PORT);

    public Zk() {
        if (!zkClient.exists(Z_NODE)) {
            zkClient.createPersistent(Z_NODE);
        }
    }

    @Override
    public void lock() {
        synchronized (this) {
            if (tryLock()) {
                System.out.println(Thread.currentThread().getName() + " 获得锁");
            } else {
                waitForLock();
                lock();
            }
        }
    }

    @Override
    public void lockInterruptibly() {

    }

    @SneakyThrows
    @Override
    public boolean tryLock() {
        // 第一次就进来创建自己的临时节点
        if (StringUtils.isBlank(path)) {
            path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock");
            System.out.println(Thread.currentThread().getName() + "创建" + path);
        }

        // 对节点排序
        List<String> children = zkClient.getChildren(Z_NODE);
        Collections.sort(children);

        // 当前的是最小节点就返回加锁成功
        if (path.equals(Z_NODE + "/" + children.get(0))) {
            return true;
        } else {
            // 不是最小节点 就找到自己的前一个 依次类推 释放也是一样
            int i = Collections.binarySearch(children, path.substring(Z_NODE.length() + 1));
            beforePath = Z_NODE + "/" + children.get(i - 1);
        }
        return false;


    }

    public void waitForLock() {
        final CountDownLatch cdl = new CountDownLatch(1);
        IZkDataListener listener = new IZkDataListener() {
            public void handleDataChange(String s, Object o) {
            }

            public void handleDataDeleted(String s) {
                cdl.countDown();
            }
        };
        // 监听
        System.out.println(Thread.currentThread().getName() + " 监听 " + beforePath);
        this.zkClient.subscribeDataChanges(beforePath, listener);
        if (zkClient.exists(beforePath)) {
            try {
                cdl.await();
                System.out.println(Thread.currentThread().getName() + " 监听到节点删除事件!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 释放监听
        zkClient.unsubscribeDataChanges(beforePath, listener);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) {
        return false;
    }

    @Override
    public void unlock() {
        System.out.println(Thread.currentThread().getName() + " 释放锁");
        zkClient.delete(path);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

使用springboot单元测试,模拟15个客户端,竞争10个订单资源。

 @Test
    void teatZkLock() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(15);
        for (int i = 0; i < 15; i++) {
            Zk zk = new Zk();
            new Thread(()->{
                zk.lock();
                // 根据id,获取订单表中的数量
                // select * from test_order where id = #{id}
                TestOrder orderById = testOrderDAO.getOrderById(1);
                if (orderById.getNums() > 0){
                    // 根据id,更新数量值
                    // update test_order set nums = #{nums} where id = #{id}
                    testOrderDAO.updateNumsByIdAndNums(1, orderById.getNums() - 1);
                }
                zk.unlock();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
    }

使用的sql没有使用事务

最终测试结果不会发生超卖

具体过程

以两个线程竞争为例

  1. Thread-3,Thread-5 创建节点
  2. Thread-3 发现自己是头节点,获得锁
  3. Thread-5 发现不是头节点,监听Thread-3
  4. Thread-3 进行业务逻辑,结束后删除节点,释放锁
  5. Thread-5 监听到Thread-3节点删除,重新竞争锁资源
  6. Thread-5 发现自己是头节点,获得锁
  7. 重复第4点过程

发现问题

回顾整个流程会发现,如果节点数量大时,需要进行排序,可能回造成一些时间延迟。
导致排好序,找到前一节点,并进行监听时,可能前一节点已删除,所以代码中lock()是一个递归函数。
若前一节点被删除还未监听时,重新竞争锁资源,直到监听到前一节点或者竞争到锁资源为止。

posted @ 2021-10-13 16:06  dxyoung  阅读(139)  评论(0)    收藏  举报