Those things about stand-alone & distributed locks

Regarding locks, there is no escape as a developer. This article is mainly to introduce the general principles of several locks used in java development. If you have any questions, welcome to Paizhuan~~

Lock under single vm

    In a java process, if we want to lock a shared resource, java provides several ways, the most commonly used are synchronized and AQS-based ReentrantLock.

· synchronized

    Synchronized is a lock implemented by JVM for us. It can lock an object and release the lock automatically by JVM. The lock provided by synchronized is a kind of reentrant lock. Reference for the introduction of reentrant locks: Click to see [Reentrant Lock Analysis]  . The general meaning of reentrant locks can be summarized as, after the program acquires the lock, it releases the lock Before, lock acquisition can be done again. The biggest benefit of this kind of lock is to solve deadlock and code reuse. for example:

public class SynchronizedTest {
    //The method is called, the object is locked
    public synchronized void method1() {
        System.out.println("method1 was called");
    }
    //The method is called, the object is locked
    public synchronized void method2() {
        method1();//The lock is acquired again and the object is locked. If synchronized does not support reentrant locks, a deadlock will occur here (the lock is occupied by the current thread, and the lock is acquired at this time)
        System.out.println("method2 was called");
    }
    public static void main(String[] args) {
        new SynchronizedTest().method2();
    }
}
 

 

    The above code not only reflects the situation of synchronized reentrant lock to avoid deadlock, but also reflects the idea of ​​code reuse. Just imagine, in order to avoid deadlock, we can write another method1 method method3 without synchronization, in method2, instead of adding synchronized method1 method, is it not enough to adjust method3? However, this exposes a method3 method that is not thread-safe, and the code is not easy to maintain.

· ReentrantLock

    This is a java-based lock provided after JDK 1.5. As the name suggests, it supports reentrancy. ReentrantLock is based on AQS (AbstractQueuedSynchronizer) and provides two implementations of fair lock and unfair lock. For example, see how to use ReentrantLock's reentrant lock:

public class ReentrantLockTest {
 
    protected ReentrantLock lock = new ReentrantLock();
 
    public void parentMethod() {
        lock.lock();
        try {
            System.out.println("parentMethod was called");
        } finally {
            lock.unlock();
        }
    }
 
    static class ReentrantLockTest2 extends ReentrantLockTest {
 
        @Override
        public void parentMethod() {
            lock.lock();
            try {
                super.parentMethod();//Here the lock is acquired again and the object is locked. If ReentrantLock does not support reentrant locks, then a deadlock will occur here (the lock is occupied by the current thread, and the lock is acquired at this time)
                doOtherThings();
            } finally {
                lock.unlock();
            }
        }
 
        public void doOtherThings() {
            System.out.println("doOtherThings was called");
        }
    }
 
    public static void main(String[] args) {
        new ReentrantLockTest2().parentMethod();
    }
}
 

    So, how does it guarantee reentrancy? Because it is implemented based on java, we can easily see the code. If you are interested, you can go to debug to track the code for locking and unlocking.
The general principle is that in AQS:
1. A variable is maintained to save the currently locked thread.
2. A state field state is maintained. This state field is used to mark the number of reentries. If it is locked for N times, then this value is N. When releasing the lock, we also need to unlock N times.
3. A queue is maintained to save threads that have not acquired locks currently. When the thread does not acquire the lock, it will be suspended and the state is waiting. Only when the thread holding the lock releases all the locks, that is, when the state is 0, will one of these waiting threads be woken up. The wake-up strategy depends on whether you use a fair lock or an unfair lock.

    Regarding the comparison between synchronized and ReentrantLock, when to use which one, there are many online information, recommend an article: Click me to see [more flexible and scalable locking mechanism in JDK 5.0]

Distributed lock

    The above describes the locks that can be used to handle shared resources in a single vm. What if we want to handle resources shared among multiple vms? Obviously neither synchronized nor ReentrantLock can meet this application scenario. We need a mechanism to resolve shared resource locking among multiple VMs. Redis and zookeeper provide some important features with which we can implement distributed locks. Below I will briefly talk about the principles of implementing distributed locks in these two ways, as well as some of the problems we often encounter.

· Redis-based distributed lock

    If we want to implement redis distributed lock, what should we do? Referring to ReentrantLock under a single VM, we need to have a lock object. We can do this:
1. Create a key that represents a lock object.
2. Using the setnx command of redis, which thread on the machine executes the command successfully, it means that the lock is acquired, otherwise, the acquisition of the lock fails.
3. Release the lock and delete the key directly.

    But in the above process, there will be a problem: Suppose a certain machine is successfully locked, and before the lock is released, it goes down, what should I do? No one releases the lock, and other threads cannot acquire the lock, resulting in starvation.
    In response to this problem, we can set a value for this key during setnx, this value is the expiration time, string type. When the thread fails to acquire the lock, it checks the value corresponding to the key, and if it finds that the time has expired, it will compete for the lock again. Assuming that the key expires, how does the thread acquire the lock again? Or use setnx? Obviously not? Are there other commands I can use? We can use the getSet command, which sets a new value for the key and returns the old value of the key. Through this feature, the way to re-acquire the lock can be realized. The procedure is roughly as follows in pseudocode given:

    public boolean tryLock(String key) {
        String expireTimeStr = String.valueOf(System.currentTimeMillis() + (1000 * 60));
        if (redis.setnx(key, expireTimeStr)) {
            //The lock is successful, return true
            return true;
        }
        //If the lock fails, get the current expiration time of the key
        String oldExpireTimeStr = redis.get(key);
        if (isTimeout(oldExpireTimeStr)) {
            String retryGetOldExpireTimeStr = redis.getSet(key, expireTimeStr);
            //If they are equal, it means that the lock has been acquired. According to the semantics of getSet, it is not difficult to understand. What is implemented here is a preemptive lock acquisition method.
            //In the case of lock competition, other threads can also execute the getSet command successfully, but we think that only this command returns the old value we expect, which means that the current thread acquires the lock
            if (retryGetOldExpireTimeStr.equals(oldExpireTimeStr)) {
                return true;
            }
        }
        return false;
    }
 
    The above method can realize the function of redis distributed lock, but it cannot satisfy reentrancy. Take a look at the pseudo code above and think about it, what would be the problem if the program used the above code to make a reentrant lock call?
· Redis-based reentrant distributed lock

    上面说的问题,很显然可能导致的后果就是锁失效。那我们应该如何设计一个可重入的redis分布式锁呢?参考ReentranLock的设计思路,我们需要在加锁时保存当前加锁的线程,以及加锁的次数。这两个信息我们需要和过期时间,一起储存到value里,value结构如下:
class LockInfo {
String EXPIRE_TIME; //锁过期时间
int COUNT; //重入次数
String CURR_THREAD; // 当前加锁线程,因为是分布式,所以这里可以为 UUID + 线程id;
……
}
我们来看看以下伪代码,如何实现重入锁加锁:

    public boolean tryLock(String key) {
        LockInfo newLockInfo = LockInfo.newInstance();
        if (redis.setnx(key, newLockInfo.toJson())) {
            //加锁成功,返回true
            return true;
        }
        //加锁失败,则获取key当前的过期时间
        LockInfo oldLockInfo = LockInfo.fromJson(redis.get(key));
        if (isTimeout(oldLockInfo)) {
            String retryGetOldExpireTimeStr = redis.getSet(key, newLockInfo.toJson());
            //如果相等,说明获取到锁,根据getSet的语义不难理解,这里实现的是一个抢占获取锁方式.
            //锁竞争情况下,其他线程也能执行getSet命令成功,但是我们认为只有这个命令返回的是我们期望的旧值,则表示当前线程获取到锁
            if (retryGetOldExpireTimeStr.equals(oldLockInfo.toJson())) {
                return true;
            }
        } else {
            //重入锁逻辑
            //是否被当前线程持有锁
            if (isLockedByCurrThread(oldLockInfo)) {
                oldLockInfo.reSetExpireTime();//重设过期时间
                oldLockInfo.incCount(1);//计数器加一
                redis.set(key, oldLockInfo.toJson());//更新
                return true;
            }
        }
        return false;
    }

 

    unlock方法,其实就是对count计数器进行减一。减一前如果count为1,则删除key,释放所有重入的锁。
    其实,redis实现分布式锁方式还是有很多问题的,如
1、加锁时间超过过期时间,如过期时间设置不合理,或者有个别情况加锁时间就是大于超时时间。
2、性能问题,获取锁失败的线程一直在sleep,然后重试,无法像java的ReentrantLock实现锁释放后的自动唤醒等待线程
    Redisson提供了以上问题的解决方案,但是个人认为有点重。点击我查看【基于Redis实现分布式锁,Redisson使用及源码分析】

    Zookeeper can naturally solve the above problems. Let's take a look at how zk implements distributed locks.

· zk-based distributed lock

    People who know zk must know that zk nodes are roughly divided into four types, one is persistent node, one is temporary node, and the other two are timing nodes of the first two.

  • Persistent node : It exists until it is created, unless it is actively deleted.
  • Temporary node : After the client is created, the life cycle of the node is consistent with the client session. When the client goes down, the node will be automatically deleted by zk.
  • Persistent timing node : Under a certain node A, the persistent timing node created, the node sequence number is incremented. A node will maintain a sequence of child nodes.
  • Temporary temporal node : Similar to persistent temporal node.

    Using the characteristics of temporary nodes, we can use it to realize the distributed lock function: the client creates a temporary node under a certain node, and if the creation is successful, it considers that the lock is acquired, otherwise the acquisition fails.
Why use temporary nodes? Because of the use of ephemeral nodes, we don't need to set an expiration time. Think about why redis needs to set the expiration time as we talked about earlier? Because the machine may be down after the lock is acquired and before the lock is released, the lock cannot be released. However, the temporary node of zk will not have this situation by nature. To emphasize, it has the feature that when the machine goes down, the temporary node is automatically deleted.

    We use temporary timing nodes to do distributed locks. The process for a thread to acquire locks is roughly as follows:

1. When locking, the client creates a time-series temporary node under a node, and the node data can be a value generated by the client (such as machine ip + thread number or UUID)
2. The client obtains all nodes under the node, view Whether the data on the lowest numbered ephemeral node is the current thread's.
     2.1. If yes, the lock is successful.
     2.2. Otherwise, the locking fails, and the monitoring node number is smaller than itself. If the monitoring node is deleted, the lock will be re-acquired. (A fair lock mechanism is implemented, that is, the order in which threads acquire locks is ordered)

    How to implement a zk-based reentrant lock?

· 基于zk的可重入分布式锁

    大致流程和以上差不多,只是我们在临时节点上多存一些数据,如当前客户端的主机信息和线程信息以及重入次数。获取锁的时候我们可判断最小节点上的主机和线程信息是否是当前加锁的主机上和线程。是的话,认为是重入了,重入次数加1。否则新创建临时节点。

其实还有一种用数据库实现的分布式锁,本文没有提及。推荐一篇文章,这篇文章中比较了几种分布式锁的优缺点,包含了数据库实现分布式锁方式,大家可看看:点击我可看【分布式锁的几种实现方式】

 

Guess you like

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