Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Redis distributed locks are implemented correctly


May 16, 2021 Redis



Distributed locks generally have database optimistic locks, Redis-based distributed locks and ZooKeeper-based distributed locks three implementation methods, and this article will bring you the second Redis-based distributed lock correct implementation methods, I hope you will be helpful.


Reliability

First, to ensure that distributed locks are available, the following four conditions must be met:

1, mutual exclusion. A t any one time, only one client can hold the lock.

2, there will be no deadlock. E ven if one client crashes while holding the lock without actively unlocking it, it is guaranteed that subsequent other clients will be able to lock.

3, with fault tolerance. C lients can be locked and unlocked as long as most of the Redis nodes are functioning properly.

4, the bell must also be tied to the bell person. L ocking and unlocking must be the same client, and the client itself cannot unlock other people's locks.


Code implementation

Introduce Jedis open source components

First we're going to introduce Jedis open source components through Maven and add the following code .xml the pom file:

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


Lock code

The correct code

Talk is cheap, show me the code。 Show the code first, and then take you slowly to explain why this is the case:

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 just one line of code: jedis.set (String key, String value, String nxxx, String expx, int time), and this set() method has five parameters:

The first is key, and we use key as a lock because key is unique.

The second is value, we pass is requestId, a lot of children's shoes may not understand, there is key as a lock is not enough, why use value? T he reason is that when we talk about reliability above, distributed locks have to be ringed to meet the fourth condition, and by assigning the value of value to requestId, we know which request the lock is added to, which can be necessary when unlocked. R equestId can be generated using the UUID.randomUUID(.toString() method.

The third is nxxx, which we fill in with NX, which means SET IF NOT EXIST, i.e. we do set operations when key does not exist;

The fourth is expx, and this parameter we pass is PX, which means that we're going to add an expired setting to this key, which is determined by the fifth parameter.

The fifth is time, echoing the fourth argument, which represents key's expiration time.

In general, executing the set() method above results in only two results:

1. If there is currently no lock (key does not exist), the locking operation is performed and an expiration date is set for the lock, while the value represents the locked client.

2. The existing lock exists and nothing is done.


Error Example 1

A more common example of an error is the use of a combination of jedis.setnx() and jedis.expire() to implement locking, 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 setnx() method is set IF NOT EXIST, and the expire() method is to add an expiration time to the lock. A t first glance it looks like the result of the previous set() method, but since these are two Redis commands, they are not atomic, and if the program suddenly crashes after the setnx() is executed, the lock does not set an expiration time. T hen there will be a deadlock. T his is achieved online because the lower version of jedis does not support the multi-parameter set() method.


Example 2 of the error

This example of an error is more difficult to find and the implementation is more complex. I mplementation idea: Use the jedis.setnx() command to implement a lock, where key is the lock and value is the expiration time of the lock. E xecution: 1. A n attempt was made to add a lock through the setnx() method, and if the current lock does not exist, the return of the lock is successful. 2 . If the lock already exists, get the expiration time of the lock, and when compared to the current time, if the lock has expired, set a new expiration time, and return the added lock successfully. The code is as follows:

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

The error in this code is:

1. Because clients generate their own expiration times, you need to force that the time for each client under distributed must be synchronized.

2. When a lock expires, if multiple clients execute the jedis.getSet() method at the same time, the expiration time of the lock for that client may be overwritten by other clients, although only one client can eventually be locked.

3. The lock does not have the owner identity, i.e. any client can be unlocked.


Unlock the code

The correct code

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, it only takes two lines of code for us to unlock! T he first line of code, we wrote a simple Lua script code, the last time we saw this programming language or in "Hacker and Painter", did not expect to use this time. I n the second line of code, we pass the Lua code to the jedis.eval() method and assign the parameter KEYS1 to lockKey and ARGV to requestId. T he eval() method is to hand over the Lua code to the Redis service side for execution.

So what does this Lua code do? I n fact, it is very simple, first get the value of the lock corresponding to the value, check whether it is equal to the requestId, if equal then remove the lock (unlock). S o why use the Lua language to implement it? B ecause make sure that the above operation is atomic. F or questions about non-atomicity, you can read Unlock Code - Error Example 2. S o why does the eval() method ensure atomicity, derived from the characteristics of Redis, in simple terms, when the eval command executes Lua code, the Lua code is executed as a command, and Redis does not execute other commands until the eval command is executed.


Error Example 1

The most common unlock code is to remove the lock directly using the jedis.del() method, which unlocks the lock directly without first judging the owner of the lock, resulting in any client being able to unlock it at any time, even if the lock is not its.

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


Error example 2

This unlock code looks good at first glance, and even I almost did it before, much like the correct posture, the only difference being that it is executed in two commands, 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 with code comments, the problem with this code is that if you call the jedis.del() method, the lock will be released when it is no longer part of the current client. S o is there really such a scenario? T he answer is yes, for example, client A is locked, after a period of time client A unlocks, before the execution of jedis.del(), the lock suddenly expires, at which point client B tries to lock successfully, and then client A executes the del() method, the lock of client B is released.


Summarize

The Redis distributed locks described in this article are implemented with JAVA, and the methods of locking and unlocking are given examples of errors for your reference. In fact, it is not difficult to achieve distributed locks through Redis, as long as the ten reliability conditions given above can be met.