Zookeeper implements distributed lock

Posted by xxATOMxx on Sat, 25 Dec 2021 11:43:00 +0100

At first, I saw the design idea of Zookeeper to realize distributed lock, which is similar to Redis written before link , a node is created through only one client. As long as there are other nodes in the node that cannot be created, a resource can be occupied. Later, I think that the lock is not just Zookeeper and Redis. Even the lock between threads in a single process is actually an object resource control. After much contact, I found that the distributed locks of Zookeeper and Redis are still different~

1, Let's take a look at some parts similar to Redis's Zookeeper distributed locks

As shown in the figure, if our three clients listen to the same node, only one client, such as client A, can be created successfully. Because the other nodes listen to node events, when A changes the node state after use, the other nodes know that they can start to seize resources again.

Herding: however, this has a disadvantage. Since many nodes listen to the same event, although only one node can seize resources every time a node change occurs, it startles the whole group and causes many event changes. Obviously, this is not cost-effective for distributed applications of many nodes.

2, Using ordered nodes to realize distributed locking

As shown in the figure, each client creates its own ordered temporary node under the lock node. The temporary node will listen to a node smaller than itself. If it is the smallest at present, it means that you can be used normally (preempting resources). Here, the extended implementation is based on the Lock provided by Java, and the watcher mechanism is added in an inherited way. The ZK operation uses the native Java API provided by ZK.

Distributed lock code
public class DistributedLock implements Lock, Watcher {
    private ZooKeeper zk=null;//Create zk client
    private String ROOT_LOCK="/locks";//Define a root node
    private String WAIT_LOCK;//Wait for the previous lock
    private String CURRENT_LOCK;//Indicates the current lock
    private CountDownLatch countDownLatch;

    public DistributedLock( ) {
        try {
            this.zk = new ZooKeeper("Ip:port", 4000, this);//Here, the watcher is implemented in this class
            //Determine whether the root node exists
            Stat stat=zk.exists(ROOT_LOCK,false);
            if (stat==null){
                zk.create(ROOT_LOCK,"0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }

    }

    @Override
    public boolean tryLock() {//When a node calls a method that attempts to obtain a lock
        try {
            //Create a temporary ordered node and copy it to the current lock
            CURRENT_LOCK=zk.create(ROOT_LOCK+"/","0".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);//Create temporary ordered node
            System.out.println(Thread.currentThread().getName()+"Create temporary node--->"+CURRENT_LOCK+"--->Attempt to acquire lock");
            //Get child node (lock node)
            List<String> childrens = zk.getChildren(ROOT_LOCK, false);
            //Create a child Set and add it to the ordered Set
            SortedSet<String> nodes=new TreeSet<>();
            for (String children:childrens){
                nodes.add(ROOT_LOCK+"/"+children);
            }
            String firstNode = nodes.first();
            //Try to obtain a smaller result set than the current node, so as to obtain the last largest node, that is, the previous node of this node
            //Note that nodes cannot be used here Last(), because the last node obtained in this way is the last node of all nodes, which is greater than this node
            SortedSet<String> lessThenMe = ((TreeSet<String>)nodes).headSet(CURRENT_LOCK);
            if (CURRENT_LOCK.equals(firstNode)){//If the current node is the smallest node in the set, it obtains a lock
                return true;
            }
            if (!lessThenMe.isEmpty()){//If there is a node smaller than the current node
                WAIT_LOCK=lessThenMe.last();//Obtain the last node of the ordered combination, that is, the previous node, and set it to WAIT_LOCL
                System.out.println("lessThenMe the last one : "+lessThenMe.last());
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public void lock() {
        if (this.tryLock()){//If the lock is obtained successfully
            System.out.println(Thread.currentThread().getName()+"Lock obtained successfully");
            return;
        }
        //If the lock is not successfully obtained, wait for the previous node to release the lock
        waitForLock(WAIT_LOCK);
    }

    private boolean waitForLock(String preNode){
        try {
            //Listen to the last node smaller than yourself
            Stat stat=zk.exists(preNode,true);
            if (stat!=null){
                System.out.println(Thread.currentThread().getName()+"wait for--->"+ROOT_LOCK+"/"+preNode+"Release lock");
                countDownLatch=new CountDownLatch(1);
                countDownLatch.await();
                System.out.println(Thread.currentThread().getName()+"--->Acquire lock");
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return true;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }


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

    @Override
    public void unlock() {
        System.out.println(Thread.currentThread().getName()+"Release lock"+CURRENT_LOCK);
        try {
            zk.delete(CURRENT_LOCK,-1);//version=-1 will be deleted anyway
            CURRENT_LOCK=null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

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

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (this.countDownLatch!=null){
            this.countDownLatch.countDown();
        }
    }
}

Test class

public class ZkDistributedLockTest {
    public static void main(String[] args) throws IOException {
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for(int i=0;i<10;i++){
            new Thread(()->{
                try {
                    countDownLatch.await();
                    DistributedLock distributedLock=new DistributedLock();
                    distributedLock.lock();//The current thread attempted to acquire a lock
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"thread "+i).start();
            countDownLatch.countDown();
        }
        System.in.read();
    }
}
Say something about your thoughts:

1. A countdown countdown latch [1] is set here to control and block concurrent threads; 2. We let each thread create a distributed lock tool class * * DistributedLock. Each thread will connect to the ZK server and become a client. There is only one creation node

3. Next, each thread tries to call our lock() method, and our lock will call the tryLock() method 4. First, let each thread create a temporary order node in tryLock(), which is independent of the thread name, and the first to arrive is sorted first

5. Then, we will find out all the sequential nodes under the distributed lock root node at this time. If the current node is the smallest node, it will obtain the lock. If the current node is not the smallest node, find the previous node and listen, set the countdown to 1 by using countdowncatch, and block the current thread [2] 6. When a listening event (deletion or change) occurs at a node, we release countdowncatch and release this thread

In this way, we can realize the sequential action of Zk nodes in the form of distributed locks.

The above provides a self implemented watcher and lock, which simply implements distributed locks using the native zk API. In fact, curator provides many highly encapsulated distributed locks, simplifies the steps, and provides more detailed operations, such as the election of disconnection in the read-write supermarket~

eg

reference: [1] Countdownlaunch parsing https://www.jianshu.com/p/a1a73ce99526 [2] Using SortedSet to sort nodes, SortedSet Headset (string key), which can take out nodes smaller than the key, SortedSet First get the first node, SortedSet Last gets the specific of the last node https://blog.csdn.net/xjk201/article/details/81586209

Ha ha ha, writing a paper is in the dark.