Correct implementation of Redis distributed lock

Posted by Draco_03 on Sat, 15 Jan 2022 14:14:47 +0100

preface

Distributed locks are generally implemented in three ways: 1 Database lock; 2. Redis based distributed locks; 3. Distributed lock based on ZooKeeper. This blog will introduce the second way to implement distributed locks based on redis. Although there are various blogs on the Internet that introduce the implementation of redis distributed lock, their implementation has various problems. In order to avoid harming people's children, this blog will introduce in detail how to correctly implement redis distributed lock.

reliability

First, in order to ensure the availability of distributed locks, we should at least ensure that the implementation of locks meets the following four conditions at the same time:

  1. Mutex. At any time, only one client can hold the lock.
  2. No deadlock occurs. Even if one client crashes while holding the lock without actively unlocking, it can ensure that other subsequent clients can lock.
  3. Fault tolerance. As long as most Redis nodes operate normally, the client can lock and unlock.
  4. Whoever unties the bell must tie it. Locking and unlocking must be the same client. The client cannot unlock the lock added by others.

code implementation

Component dependency

First of all, we will introduce Jedis open source components through Maven, which can be found in POM. Com Add the following code to the XML file:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

Lock code

Correct posture

Talk is cheap, show me the code. First show the code, and then slowly explain why it is implemented in this way:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * Attempt to acquire distributed lock
     * @param jedis Redis client
     * @param lockKey lock
     * @param requestId Request ID
     * @param expireTime Overdue time
     * @return Is it successful
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

As you can see, we lock in one line of code: jedis Set (string key, string value, string nxxx, string expx, int time). This set() method has five formal parameters:

  • The first one is the key. We use the key as the lock because the key is unique.

  • The second is value. We pass requestId. Many children's shoes may not understand it. Isn't it enough to have a key as a lock? Why use value? The reason is that when we talk about reliability above, the distributed lock must meet the fourth condition. The person who tied the bell must unlock the lock. By assigning value to requestId, we can know which request added the lock and have a basis when unlocking. requestId can use UUID randomUUID(). Generated by tostring() method.

  • The third parameter is nxxx. NX is filled in this parameter, which means SET IF NOT EXIST, that is, when the key does not exist, we perform set operation; If the key already exists, no operation will be performed;

  • The fourth parameter is expx. We pass PX, which means that we need to add an expiration setting to the key. The specific time is determined by the fifth parameter.

  • The fifth is time, which corresponds to the fourth parameter and represents the expiration time of the key.

In general, executing the set() method above will only lead to two results: 1 If there is no lock at present (the key does not exist), then lock it and set a valid period for the lock. At the same time, value represents the locked client. 2. The existing lock exists and no operation is required.

Careful children's shoes will find that our locking code meets the three conditions described in our reliability. First, the NX parameter is added to set() to ensure that if there is an existing key, the function will not be called successfully, that is, only one client can hold the lock and meet mutual exclusion. Secondly, because we have set the expiration time for the lock, even if the lock holder crashes and does not unlock it later, the lock will be unlocked automatically (i.e. the key will be deleted) and deadlock will not occur. Finally, because we assign value to requestId, which represents the request ID of the locked client, we can verify whether it is the same client when the client is unlocked. Since we only consider the scenario of Redis single machine deployment, we will not consider the fault tolerance temporarily.

Error example 1

A common error example is using jedis Setnx() and jedis The combination of expire() implements locking, and the code is as follows:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // If the program crashes suddenly here, the expiration time cannot be set and a deadlock will occur
        jedis.expire(lockKey, expireTime);
    }

}

The setnx() method is used to SET IF NOT EXIST, and the expire() method is used to add an expiration time to the lock. At first glance, it seems to be the same as the result of the previous set() method. However, because these are two Redis commands and are not atomic, if the program crashes suddenly after executing setnx(), the lock does not set the expiration time. Then a deadlock will occur. The reason why people implement this on the Internet is that the lower version of jedis does not support multi parameter set() method.

Error example 2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // If the current lock does not exist, it returns success in locking
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // If the lock exists, get the expiration time of the lock
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // The lock has expired. Obtain the expiration time of the previous lock and set the expiration time of the current lock
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // Considering multithreading concurrency, only when the setting value of a thread is the same as the current value, it has the right to lock
            return true;
        }
    }
        
    // In other cases, locking failure is returned
    return false;

}

This kind of error example is more difficult to find problems, and the implementation is also more complex. Implementation idea: use jedis The setnx() command implements locking, where key is the lock and value is the expiration time of the lock. Execution process: 1 Try locking through the setnx() method. If the current lock does not exist, it returns that locking succeeded. 2. If the lock already exists, obtain the expiration time of the lock and compare it with the current time. If the lock has expired, set a new expiration time and return the success of locking. The code is as follows:

 

So what's the problem with this code? 1. Since the expiration time is generated by the client itself, it is mandatory that the time of each client must be synchronized under distributed. 2. When the lock expires, if multiple clients execute jedis at the same time GetSet () method, although only one client can lock in the end, the expiration time of this client's lock may be overwritten by other clients. 3. The lock does not have owner ID, that is, any client can be unlocked.

unlock code

Correct posture

Let's show the code first, and then slowly explain why it is implemented in this way:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * Release distributed lock
     * @param jedis Redis client
     * @param lockKey lock
     * @param requestId Request ID
     * @return Release successful
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

As you can see, we only need two lines of code to unlock! In the first line of code, we wrote a simple Lua script code. The last time we saw this programming language was in hacker and painter. Unexpectedly, it was used this time. The second line of code, we pass Lua code to jedis eval() method, and assign the parameter KEYS[1] to lockKey and ARGV[1] to requestId. The eval () method is to hand over the Lua code to the Redis server for execution.

So what is the function of this Lua code? In fact, it is very simple. First, obtain the value corresponding to the lock, check whether it is equal to requestId, and delete the lock (unlock) if it is equal. So why use Lua language to implement it? Because it is necessary to ensure that the above operations are atomic. You can read about the problems of non atomicity [unlock code - error example 2]  . So why implementing the eval() method can ensure atomicity stems from the Redis feature. Here are some explanations of the eval command on the official website:

Simply put, when the eval command executes Lua code, Lua code will be executed as a command, and Redis will not execute other commands until the eval command is executed.

Error example 1

The most common unlocking code is to directly use jedis Del () method deletes the lock. This method of directly unlocking the lock without first judging the owner of the lock will cause any client to unlock it at any time, even if the lock is not its own.

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

Error example 2

At first glance, this unlocking code is OK. Even I almost realized it before. It is similar to the correct posture. The only difference is that it is divided into two commands to execute. The code is as follows:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // Judge whether locking and unlocking are the same client
    if (requestId.equals(jedis.get(lockKey))) {
        // If the lock is suddenly not owned by the client at this time, it will be unlocked by mistake
        jedis.del(lockKey);
    }

}

As the code notes, the problem is that if jedis. Is called When the del() method is used, the lock added by others will be released when the lock does not belong to the current client. So is there really such A scene? The answer is yes. For example, client A locks. After A period of time, client A unlocks and executes jedis Before del(), the lock suddenly expires. At this time, client B attempts to lock successfully, and then client A executes the del() method to unlock client B.

Reprinted to: https://www.cnblogs.com/williamjie/p/9395659.html

Topics: Java Redis Distributed lock