Redis implements distributed locks

Original source:  Wu Zhaofeng

foreword

There are generally three ways to implement distributed locks: 1. Database optimistic locks; 2. Redis-based distributed locks; 3. ZooKeeper-based distributed locks. 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 locks, their implementations have various problems. In order to avoid misunderstandings, this blog will introduce in detail how to correctly implement Redis distributed locks.


reliability

First, in order to ensure that distributed locks are available, we must at least ensure that the implementation of the lock meets the following four conditions:

  1. mutual exclusivity. At any time, only one client can hold the lock.
  2. No deadlock will occur. Even if a client crashes while holding the lock and does not actively unlock it, other clients can be guaranteed to lock in the future.
  3. Fault tolerant. Clients can lock and unlock as long as most of the Redis nodes are up and running.
  4. The trouble should end it. Locking and unlocking must be done by the same client, and the client cannot unlock the locks added by others.

Code

component dependencies

First, we need to introduce Jedisopen source components through Maven, and pom.xmladd the following code to the 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. Show the code first, and then take you to slowly explain why it is implemented like this:

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";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    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 with one line of code: jedis.set(String key, String value, String nxxx, String expx, int time), this set() method has a total of 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, is it not enough to have key as a lock, why use value? The reason is that when we talked about reliability above, the distributed lock must meet the fourth condition to unlock the ringer , and by assigning the value as requestId, we know which request the lock was added. can have a basis. requestId can be UUID.randomUUID().toString()generated using the method.

  • The third is nxxx, we fill in NX for this parameter, which means SET IF NOT EXIST, that is, when the key does not exist, we perform the set operation; if the key already exists, do nothing;

  • The fourth one is expx, which we pass as PX, which means that we need to add an expired setting to this key, and 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 above set() method will only lead to two results: 1. There is no lock at present (the key does not exist), then the locking operation is performed, and a validity period is set for the lock, and the value indicates the lock client. 2. There is a lock, do nothing.

Careful children's shoes will find that our lock code meets the three conditions described in our reliability . First of all, set() adds the NX parameter, which can ensure that if there is an existing key, the function will not be called successfully, that is, only one client can hold the lock, which satisfies mutual exclusion. Second, since we set an expiration time for the lock, even if the lock holder crashes and fails to unlock it, the lock will be automatically unlocked (that is, the key will be deleted) due to the expiration time, and no deadlock will occur. Finally, because we assign the value to requestId, which represents the locked client request ID, we can check whether it is the same client when the client is unlocked. Since we only consider the scenario of single-machine deployment of Redis, we do not consider fault tolerance for the time being.

Error example 1

A common error example is to use jedis.setnx()and jedis.expire()combine to achieve locking, 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) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}

The function of the setnx() method is SET IF NOT EXIST, and the expire() method is 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, since these are two Redis commands, they are not atomic. If the program crashes suddenly after executing setnx(), the lock does not have an expiration time set. Then a deadlock will occur. The reason why someone implements this on the Internet is because the lower version of jedis does not support the multi-parameter set() method.

Error example 2

This kind of error example is more difficult to find, and the implementation is more complicated. Implementation idea: use the jedis.setnx()command to realize the lock, where the key is the lock, and the value is the expiration time of the lock. Execution process: 1. Try to lock through the setnx() method. If the current lock does not exist, it will return the lock successfully. 2. If the lock already exists, get 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 lock success. code show as below:

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

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

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}

So where is the problem with this code? 1. Since the client generates the expiration time by itself, it is necessary to force the time of each client in the distributed environment to be synchronized. 2. When the lock expires, if multiple clients execute the jedis.getSet()method at the same time, then although only one client can lock, the expiration time of this client's lock may be overwritten by other clients. 3. The lock does not have an owner ID, that is, any client can unlock it.

unlock code

correct posture

Or show the code first, and then take you to explain why it is implemented in this way:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    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 it! The first line of code, we wrote a simple Lua script code, the last time I saw this programming language was in "Hackers and Painters", I didn't expect it to be used this time. In the second line of code, we pass the Lua code to jedis.eval()the method, and assign the parameter KEYS[1] to lockKey and ARGV[1] to requestId. The eval() method is to hand 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 the requestId, and delete the lock (unlock) if it is equal. So why use Lua language to implement it? Because the above operation is guaranteed to be atomic. For what problems non-atomicity can bring, you can read [Unlock Code - Error Example 2]  . So why the execution of the eval() method can ensure atomicity is due to the characteristics of Redis. The following is a partial explanation of the eval command on the official website:

Simply put, when the eval command executes the Lua code, the 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 unlock code is to use the jedis.del()method to delete the lock directly. This method of unlocking the lock without first determining the owner of the lock will allow any client to unlock at any time, even if the lock is not his.

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

Error example 2

At first glance, this kind of unlocking code is no problem. 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) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}

As in the code comments, the problem is that if jedis.del()the method is called, the lock will be released when the lock no longer belongs to the current client. So is there really such a scenario? The answer is yes. For example, when client A locks, and after a period of time, client A unlocks, the jedis.del()lock suddenly expires before execution. At this time, client B tries to lock successfully, and then client A executes the del() method. Then the lock of client B is released.


Summarize

This article mainly introduces how to use Java code to correctly implement Redis distributed locks, and also gives two classic error examples for locking and unlocking. In fact, it is not difficult to implement distributed locks through Redis, as long as the four conditions in reliability are guaranteed. Although the Internet has brought us convenience, as long as we have a question, we can google it, but must the answer on the Internet be correct? In fact, it is not, so we should always maintain the spirit of questioning, think more and verify more.

If Redis is deployed on multiple machines in your project, you can try to Redissonimplement distributed locks. This is the official Java component provided by Redis. The link is given in the reference section.


reference reading

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson




Guess you like

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