编程知识 cdmana.com

Redis or zookeeper is better for distributed lock?

Click on the above “ Zhu Xiaosi's blog ”, choice “ Set to star ”

The background to reply " book ", obtain

The background to reply “k8s”, Can claim k8s Information

source :r6d.cn/W3Gy

When it comes to locks, you must know , image Synchronized、ReentrantLock, In the case of a single process , Multiple threads access the same resource , You can use them to keep threads safe .

However, at present, more and more Internet projects adopt cluster deployment , That's the distributed situation , These two kinds of locks are not enough .

Let's take two pictures to illustrate , In the case of local locks :

In the case of distributed locks :

In terms of its ideas , It's a kind of “ All I want ” Thought , All services come to a unified place to get locks , Only those who get the lock can continue to execute .

Finish thinking , Let's talk about the specific implementation .

Redis Realization

To implement distributed locks , stay Redis in SETNX key value command , Meaning for set if not exists( If it doesn't exist key, Just to go set value ), For example, Zhang San went to the toilet , Look, the toilet is locked , He won't go in , He didn't go until the toilet door was open .

You can see , for the first time set Back to 1, It means success , But the second time back 0, Express set Failure , Because there is already this key 了 .

Of course, it's just setnx Is this order OK ? Of course not , Imagine a situation , Zhang San is in the toilet , But he hasn't been released in it , Squatting in it all the time , No one outside wants to go to the toilet , Trying to hammer him to death .

Redis Empathy , Suppose locking has been done , But the lock is not released because of downtime or exception , That's what it's called “ Deadlock ”.

Smart you must have thought of it , Just set the expiration time for it , Sure SETEX key seconds value command , For a given key Set expiration time , The unit is in seconds .

But there's another problem , I just locked it , Expiration time has not been set ,Redis When it goes down, it's deadlocked again , So we should ensure the atomicity , Or make it together , Or fail together .

Of course we can think of Redis It must have been done for you , stay Redis 2.8 After the version of the ,Redis It gives us a combined command SET key value ex seconds nx, Lock at the same time set the expiration time .

It's like the company rules that everyone can only stay in the toilet at most 2 minute , Whether it's released or not, it has to be released , That's it “ Deadlock ” problem .

But then there's no problem ? How is that possible? .

Imagine another situation , The toilet door can only be opened from the inside , After Zhang San went to the toilet, Zhang Si went in and locked the door , But people outside think it's still Zhang San inside , And it's been 3 Minutes. , I just pried the door open , It's a four inside , It's embarrassing .

Switch to Redis That is to say, a business takes a long time to execute , The lock has expired by itself , Someone else has set up a new lock , But when the business is finished, release the lock directly , It's possible that someone else's lock has been deleted , Isn't that a mess .

So when locking , To set a random value , Compare when deleting locks , If it's your own lock , To delete .

It's no use saying more , annoying , Go straight to the code :

// be based on jedis and lua Script to achieve 
privatestaticfinal String LOCK_SUCCESS = "OK";
privatestaticfinal Long RELEASE_SUCCESS = 1L;
privatestaticfinal String SET_IF_NOT_EXIST = "NX";
privatestaticfinal String SET_WITH_EXPIRE_TIME = "PX";

@Override
public String acquire() {
    try {
        //  The timeout for obtaining the lock , After this time, the lock is abandoned 
        long end = System.currentTimeMillis() + acquireTimeout;
        //  Randomly generate one  value
        String requireToken = UUID.randomUUID().toString();
        while (System.currentTimeMillis() < end) {
            String result = jedis
                .set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return requireToken;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    } catch (Exception e) {
        log.error("acquire lock due to error", e);
    }

    returnnull;
}

@Override
public boolean release(String identify) {
    if (identify == null) {
        returnfalse;
    }
    // adopt lua Script to compare and delete , Guaranteed atomicity 
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = new Object();
    try {
        result = jedis.eval(script, Collections.singletonList(lockKey),
            Collections.singletonList(identify));
        if (RELEASE_SUCCESS.equals(result)) {
            log.info("release lock success, requestToken:{}", identify);
            returntrue;
        }
    } catch (Exception e) {
        log.error("release lock due to error", e);
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }

    log.info("release lock failed, requestToken:{}, result:{}", identify, result);
    returnfalse;
}

reflection : The atomicity of lock and release lock can be used lua Script to ensure , How can the automatic renewal of the lock be realized ?

Redisson Realization

Redisson seeing the name of a thing one thinks of its function ,Redis Son , In essence Redis Lock , Just right Redis A lot of encapsulation , It not only provides a series of distributed Java Common objects , There are also many distributed services .

In the introduction of Redisson After our dependence , You can call it directly :

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

Let's start with Redisson Lock code for :

private void test() {
    // Distributed lock name    The finer the granularity of the lock , The better the performance 
    RLock lock = redissonClient.getLock("test_lock");
    lock.lock();
    try {
        // Specific business ......
    } finally {
        lock.unlock();
    }
}

It's that simple , Usage method jdk Of ReentrantLock almost , And it supports ReadWriteLock( Read-write lock )、Reentrant Lock( Reentrant lock )、Fair Lock( Fair lock )、RedLock( Red lock ) All kinds of locks , For details, please refer to redisson Official documents to see .

that Redisson What are the advantages ? Automatic renewal of locks ( The default is 30 second ), If the business is too long , The lock will be automatically renewed during operation 30s, Don't worry that the lock will be automatically deleted when the business execution time is too long .

Lock the business as long as the operation is completed , There will be no renewal of the current , Even without manual unlocking , By default, the lock is in 30s Delete after , No deadlock problem .

I also mentioned the automatic renewal of locks , Let's see Redisson How to achieve .

Let's start with , Here's the main thing Redisson Medium RLock, It's a reentrant lock , There are two ways to do it :

//  The most common use method 
lock.lock();

//  After the lock 10 Seconds auto unlock 
//  Don't need to call unlock Method to manually unlock 
lock.lock(10, TimeUnit.SECONDS);

The only method without parameters is to provide automatic renewal of lock , Internal use is “ watchdog ” Mechanism , Let's take a look at the source code .

Whether it's a null parameter or a method with parameters , They all call the same lock Method , If you don't pass the parameters, you've passed one -1, And the method with parameters passes the actual time .

Keep going scheduleExpirationRenewal Method :

Click in renewExpiration Method :

To sum up , When we specify the lock expiration time , Then the lock will be released automatically when it comes to time . If no lock expiration time is specified , Just use the default time of the watchdog 30s, As long as the lock is successful , Will start a timed task , every other 10s Set a new expiration time for the lock , Time is the default time of the watchdog , Until the lock is released .

Summary : although lock() There is an automatic renewal mechanism , But it's still recommended in development lock(time,timeUnit), Because it saves the performance loss of the whole renewal , You can set the expiration time a little longer , collocation unlock().


If business execution is completed , Will release the lock manually , If the business execution times out , In general, our service will also set the business timeout , It's just a mistake , After the error is reported, the lock will be released through the set expiration time .

public void test() {
    RLock lock = redissonClient.getLock("test_lock");
    lock.lock(30, TimeUnit.SECONDS);
    try {
        //....... Specific business 
    } finally {
        // Release the lock manually 
        lock.unlock();
    }
}

be based on Zookeeper To implement distributed locking

Many kids know that in a distributed system , It can be used ZK To be a registry , But in fact, in addition to being an ancestral Center , use ZK Distributed lock is also a common solution .

Let's take a look first ZK How to create a node in ?ZK in create [-s] [-e]  path [data] command ,-s To create ordered nodes ,-e Create temporary nodes .

This creates a parent node and a child node for the parent node , The combined command means to create a temporary ordered node .

and ZK Distributed lock is mainly realized by creating temporary sequential nodes . Why use sequential nodes and why use temporary nodes instead of persistent nodes ? Think about it first , This is explained below .

As well as ZK How to view nodes in ?ZK in ls [-w] path To view node commands ,-w Add a watch( The monitor ),/ To view all nodes of the root node , You can see the node we just created , At the same time, if it is followed by the name of the specified node, it is to view the child nodes under the specified node .

hinder 00000000 by ZK The order added to the order node . Registered listeners are also ZK One of the most important things in implementing distributed locks .

So let's see ZK The main process of implementing distributed lock :

  • When the first thread comes in, it will go to the parent node to create a temporary sequential node .

  • The second thread comes in and finds that the lock has been held , Will register a... For the node that currently holds the lock watcher Monitor .

  • The third thread comes in and finds that the lock has been held , Because it's a sequential node , It will create one for the previous node watcher Monitor .

  • When the first thread releases the lock , Delete node , The lock is held by its next node .

See here , Smart guys have seen the benefits of sequential nodes . Non sequential nodes , Every thread that comes in will register a listener on the node that holds the lock , Easy to trigger “ Herd behaviour ”.

Such a large flock of sheep are coming to you , Whether you can stand it or not , Anyway ZK Servers increase the risk of downtime .

And sequential nodes don't , When the sequence node finds that a thread already holds a lock , It registers a listener with its previous node , So when the node holding the lock is released , Only the next node holding the lock can seize the lock , It's equivalent to queuing up for execution , Reduce the risk of server downtime .

As for why temporary nodes are used , and Redis There's a reason for the expiration time of , Even if the ZK Server down , Temporary nodes will disappear with the server down , The deadlock situation is avoided .

Here is the implementation of the previous code :

public class ZooKeeperDistributedLock implements Watcher {

    private ZooKeeper zk;
    private String locksRoot = "/locks";
    private String productId;
    private String waitNode;
    private String lockNode;
    private CountDownLatch latch;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
    private int sessionTimeout = 30000;

    public ZooKeeperDistributedLock(String productId) {
        this.productId = productId;
        try {
            String address = "192.168.189.131:2181,192.168.189.132:2181";
            zk = new ZooKeeper(address, sessionTimeout, this);
            connectedLatch.await();
        } catch (IOException e) {
            throw new LockException(e);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public void process(WatchedEvent event) {
        if (event.getState() == KeeperState.SyncConnected) {
            connectedLatch.countDown();
            return;
        }

        if (this.latch != null) {
            this.latch.countDown();
        }
    }

    public void acquireDistributedLock() {
        try {
            if (this.tryLock()) {
                return;
            } else {
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }
    // Get the lock 
    public boolean tryLock() {
        try {
        //  It's coming in locksRoot + “/” + productId
        //  hypothesis productId Represents a commodity id, for instance 1
        // locksRoot = locks
        // /locks/10000000000,/locks/10000000001,/locks/10000000002
        lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        //  See if the newly created node is the smallest one 
        // locks:10000000000,10000000001,10000000002
        List<String> locks = zk.getChildren(locksRoot, false);
        Collections.sort(locks);

        if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
            // If it's the smallest node , It means to obtain the lock 
            return true;
        }

        // If it's not the smallest node , Find something smaller than yourself 1 The node of 
      int previousLockIndex = -1;
            for(int i = 0; i < locks.size(); i++) {
        if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
                    previousLockIndex = i - 1;
            break;
        }
       }

       this.waitNode = locks.get(previousLockIndex);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }

    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
        if (stat != null) {
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
            this.latch = null;
        }
        return true;
    }

    // Release the lock 
    public void unlock() {
        try {
            System.out.println("unlock " + lockNode);
            zk.delete(lockNode, -1);
            lockNode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
    // abnormal 
    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public LockException(String e) {
            super(e);
        }

        public LockException(Exception e) {
            super(e);
        }
    }
}

summary

Now that I understand Redis and ZK The implementation of distributed lock respectively , So it should be different . you 're right , I've sorted it out for you :

  • Different ways of implementation ,Redis The implementation is to insert a piece of occupied data , and ZK The implementation is to register a temporary node .

  • In case of downtime ,Redis Need to wait until the expiration time to automatically release the lock , and ZK Because it's a temporary node , At the time of downtime, the node has been deleted to release the lock .

  • Redis In the case of not seizing the lock, we usually spin to get the lock , It's a waste of performance , and ZK The way to get the listener is to register , In terms of performance, it is better than Redis.

But what kind of implementation should be adopted , We still need to analyze the specific situation , Combined with the technology stack referenced by the project to realize the landing .

Want to know more ? sweep Trace the QR code below and follow me

The background to reply " technology ", Join the technology group

The background to reply “k8s”, Can claim k8s Information

【 Highlights 】

Point a praise + Looking at , Less bug ????

版权声明
本文为[Zhu Xiaosi]所创,转载请带上原文链接,感谢
https://cdmana.com/2021/01/20210101074837456i.html

Scroll to Top