Redis distributed locking case

Posted by 88fingers on Fri, 17 Dec 2021 11:05:33 +0100

1. 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>

2. Locking code

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

//java project fhadmin cn
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";
 
 
 
    /**java Project fhadmin cn
     * 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), perform the lock operation and set a valid period for the lock. Meanwhile, value indicates the locked client. 2. The existing lock exists and no operation is performed.

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 set the expiration time for the lock, even if the lock holder crashes and does not unlock it later, The lock will also be unlocked automatically when it expires (that is, the key is deleted) and no deadlock will occur. Finally, because we assign value to requestId, which represents the request ID of the locked client, we can verify whether the client 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.

3. 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:

//java project fhadmin cn
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.

4. Error example 2

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

//java project fhadmin cn
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;
 
}

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 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 the owner ID, that is, any client can be unlocked.

Topics: Java Redis Distribution