Zookeeper implements a distributed lock from 0 to 1

[Middleware] Zookeeper implements a distributed lock from 0 to 1

Distributed locks are commonly used in actual business usage scenarios, and the implementation of distributed locks, except for redis, is the implementation of zk. The previous blog post introduced the basic concepts and usage of zk. , then if we design a distributed lock with the features of zk in mind, what can we do?

<!-- more -->

I. Schematic Design

1. Create node implementation

There are four kinds of nodes in zk. One of the easiest strategies to think of is to create a node. Whoever creates it successfully means who holds the lock.

This idea is a setnxbit similar to redis, because only one session will be created successfully when the zk node is created, and the others will throw existing exceptions.

With the help of the temporary node, the node is deleted after the session is lost, which can avoid the problem that the instance holding the lock is abnormal without actively releasing, so that all the instances cannot hold the lock.

If this scheme is adopted, if I want to implement the logic of blocking the lock acquisition, then one of the schemes needs to write a while(true) to keep retrying

while(true) {
    if (tryLock(xxx)) return true;
    else Thread.sleep(1000);
}

Another strategy is to use event monitoring. When a node exists, register a trigger for node deletion, so that I don't need to retry and judge myself; make full use of the features of zk to achieve asynchronous callbacks

public void lock() {
  if (tryLock(path,  new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            synchronized (path){
                path.notify();
            }
        }
    })) {
      return true;
  }

  synchronized (path) {
      path.wait();
  }
}

So what's wrong with the above implementation?

Every time the node changes, all the changes will be monitored. The advantage is the support of unfair locks; the disadvantage is that only one of the remaining wake-up instances will preempt the lock, meaningless wake-up wastes performance

2. Temporary sequential node method

Next, this kind of scheme is more common. Most of the tutorials in the evening are also this kind of case. The main idea is to create a temporary sequence node.

Only the node with the smallest sequence number indicates that the preemption lock is successful; if it is not the smallest node, then listen to the deletion event of the previous node, and the previous node is deleted. There are locks, no matter what the situation is, for me, I need to fish all the nodes, either get the lock successfully; or change a pre-node

II. Distributed lock implementation

Next, let's take a step-by-step look at how to implement distributed locks based on temporary sequential nodes

For zk, we still use the package provided by apache zookeeperto operate; Curatorthe distributed lock instance provided later

1. Dependency

core dependencies

<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.7.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Release Notes:

  • zk version: 3.6.2
  • SpringBoot: 2.2.1.RELEASE

2. Simple distributed lock

The first step is to create an instance

public class ZkLock implements Watcher {

    private ZooKeeper zooKeeper;
    // 创建一个持久的节点,作为分布式锁的根目录
    private String root;

    public ZkLock(String root) throws IOException {
        try {
            this.root = root;
            zooKeeper = new ZooKeeper("127.0.0.1:2181", 500_000, this);
            Stat stat = zooKeeper.exists(root, false);
            if (stat == null) {
                // 不存在则创建
                createNode(root, true);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    
    // 简单的封装节点创建,这里只考虑持久 + 临时顺序
    private String createNode(String path, boolean persistent) throws Exception {
        return zooKeeper.create(path, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, persistent ? CreateMode.PERSISTENT : CreateMode.EPHEMERAL_SEQUENTIAL);
    }
}

In our design, we need to hold the current node and monitor the changes of the previous node, so we add two members to the ZkLock instance

/**
 * 当前节点
 */
private String current;

/**
 * 前一个节点
 */
private String pre;

Next is the logic of trying to acquire the lock

  • If current does not exist, if it indicates that it has not been created, create a temporary sequence node and assign current
  • If current exists, it means that it has been created before and is currently in the process of waiting for the lock to be released.
  • Next, according to whether the current node order is the smallest, to indicate whether the lock is held successfully
  • When the order is not the smallest, find the previous node and assign pre;
  • Monitor changes in pre
/**
 * 尝试获取锁,创建顺序临时节点,若数据最小,则表示抢占锁成功;否则失败
 *
 * @return
 */
public boolean tryLock() {
    try {
        String path = root + "/";
        if (current == null) {
            // 创建临时顺序节点
            current = createNode(path, false);
        }
        List<String> list = zooKeeper.getChildren(root, false);
        Collections.sort(list);

        if (current.equalsIgnoreCase(path + list.get(0))) {
            // 获取锁成功
            return true;
        } else {
            // 获取锁失败,找到前一个节点
            int index = Collections.binarySearch(list, current.substring(path.length()));
            // 查询当前节点前面的那个
            pre = path + list.get(index - 1);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

Please pay attention to the above implementation, there is no monitoring of the changes of the previous node. In the design tryLock, because it returns success or failure immediately, using this interface does not need to register monitoring

Our monitoring logic is placed in lock()synchronous blocking

  • Attempt to preempt the lock, and return directly if successful
  • If the lock fails, listen for the delete event of the previous node
public boolean lock() {
    if (tryLock()) {
        return true;
    }

    try {
        // 监听前一个节点的删除事件
        Stat state = zooKeeper.exists(pre, true);
        if (state != null) {
            synchronized (pre) {
                // 阻塞等待前面的节点释放
                pre.wait();
                // 这里不直接返回true,因为前面的一个节点删除,可能并不是因为它持有锁并释放锁,如果是因为这个会话中断导致临时节点删除,这个时候需要做的是换一下监听的 preNode
                return lock();
            }
        } else {
          // 不存在,则再次尝试拿锁
          return lock();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

Notice:

  • When the node does not exist, or after the event triggers the callback, it is called lock()again, indicating that Hu Hansan is competing for the lock again?

Why not return true directly? Instead, it needs to re-compete?

  • Because of the deletion of the previous node, it may be caused by the interruption of the session of the previous node; but the lock is still in the hands of another instance, what I should do at this time is to re-queue

Don't forget to release the lock at the end

public void unlock() {
    try {
        zooKeeper.delete(current, -1);
        current = null;
        zooKeeper.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

At this point, our distributed lock is completed, and then we review the implementation process

  • All knowledge points come from the basic use of zk in the previous article (create nodes, delete nodes, get all your own points, listen for events)
  • Lock grabbing process = "Create the node with the smallest serial number
  • If the node is not the smallest, then listen to the previous node deletion event

This implementation supports the reentrancy of locks (why? Because when the lock is not released, we save the current, and when the current node exists, we directly judge whether it is the smallest; instead of re-creating)

3. Test

Finally, write a test case, take a look

@SpringBootApplication
public class Application {

    private void tryLock(long time) {
        ZkLock zkLock = null;
        try {
            zkLock = new ZkLock("/lock");
            System.out.println("尝试获取锁: " + Thread.currentThread() + " at: " + LocalDateTime.now());
            boolean ans = zkLock.lock();
            System.out.println("执行业务逻辑:" + Thread.currentThread() + " at:" + LocalDateTime.now());
            Thread.sleep(time);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (zkLock != null) {
                zkLock.unlock();
            }
        }
    }

    public Application() throws IOException, InterruptedException {
        new Thread(() -> tryLock(10_000)).start();

        Thread.sleep(1000);
        // 获取锁到执行锁会有10s的间隔,因为上面的线程抢占到锁,并持有了10s
        new Thread(() -> tryLock(1_000)).start();
        System.out.println("---------over------------");

        Scanner scanner = new Scanner(System.in);
        String ans = scanner.next();
        System.out.println("---> over --->" + ans);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

}

The output is as follows

II. Other

0. Project

1. A gray blog

It is not as good as a letter. The above content is purely from the family. Due to limited personal ability, there are inevitably omissions and mistakes. If you find bugs or have better suggestions, you are welcome to criticize and correct them. Thank you very much.

The following is a gray personal blog, recording all blog posts in study and work, welcome everyone to visit

a grey blog

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324152464&siteId=291194637