80% of people do not know the proper implementation of Redis Distributed Lock (Java version)

This blog using third party open source components Jedis achieve Redis client, and only consider the scene Redis server stand-alone deployment.

Foreword

Distributed Lock There are three general ways: 1 database optimistic locking; 2-based distributed lock the Redis; 3 based on the Distributed Lock ZooKeeper.... This blog will introduce the second way, Redis achieve Distributed Lock based. Although the Internet has introduced a variety of distributed lock Redis implemented blog, but they realize they have a variety of problems, in order to avoid fraught, this blog will detail how to implement Redis distributed lock properly.

reliability

First, in order to ensure that the distributed lock is available, we at least want to ensure the lock at the same time meet the following four conditions:

  1. Mutually exclusive. At any time, only one client can hold the lock.

  2. A deadlock does not occur. Even with a client during a crash without holding the lock to unlock the initiative, but also to ensure follow-up to other clients can lock.

  3. Fault tolerant. As long as most of the Redis functioning node, the client can lock and unlock.

  4. The trouble should end it. Locking and unlocking must be the same client, the client's own people can not be added to the solution of the lock.

Code

Component depends

First, we want to introduce a Maven Jedisopen source components in the pom.xmlfile and add the following code:

<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 impressions code, and then slowly bring you explain why this realization:

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;

    }

}

We can see, we are locked on one line of code: jedis.set(String key, String value, String nxxx, String expx, int time)this set () method, a total of five parameter:

  • The first is the key, when we use the key to lock because the key is unique.

  • The second is the value, we preach is requestId, a lot of children's shoes may not understand, there is a key lock is not enough yet, why use value? The reason is that when we talked about the reliability of the above, distributed lock to meet the fourth condition started the trouble should end it , by giving value assigned to requestId, we know that this lock is which request added, and in the unlocked when we can have a basis. requestId may be used UUID.randomUUID().toString()a method of generating.

  • The third is nxxx, we fill this parameter is NX, meaning that SET IF NOT EXIST, that is, when the key is not present, we set operations; if the key already exists, do nothing;

  • The fourth is expx, we pass this parameter is PX, meaning that we give this key plus an expired set the specific time decided by the fifth parameter.

  • The fifth is the time, and the fourth argument echoes, representatives of key expiration time.

Overall, the implementation of the above set () method will only lead to two results: 1. There are currently no lock (key does not exist), then the process of locking operation and a lock setting period, while value is the lock clients. 2. have been latched, does nothing.

Cautious of children's shoes will be found, we lock code meets our reliability three conditions described in. First, set () joined the NX parameters, you can guarantee that if there is an existing key, the function will not be called successful, that is, only one client can hold a lock to meet mutually exclusive. Secondly, because we set the expiration time of the lock, even if the lock holder subsequent collapse occurred without unlock the lock will be because of the expiration time and automatically unlock (ie key is deleted), the deadlock will not occur. Finally, because we value assigned to requestId, on behalf of the client request identification lock, then the client can be unlocked at the time of whether the check is the same client. Since we only consider Redis standalone deployment scenario, so we will not consider fault tolerance.

Error Example 1

More common example is the use of an error jedis.setnx()and jedis.expire()a combination lock, 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);
    }

}

setnx () method function is to SET IF NOT EXIST, expire () method is to add a lock expiration time. At first glance, it seems the previous set () method as a result, however, since this is two Redis commands, having no atomic, if the sudden collapse in the program after performing SETNX (), resulting in the lock is not set the expiration time. Then the deadlock will occur. The reason why the Internet was achieved, because of the low version of jedis does not support multi-parameter set () method.

Error Example 2

Examples of this kind of error is more difficult to find the problem, but also more complicated to achieve. Realization of ideas: Use the jedis.setnx()command to achieve lock, which is a key lock, value is the expiration time lock. Execution: 1. () method attempts by setnx lock, if the current key is not present, return lock success. 2. expiration of the time to acquire the lock if the lock already exists, and the current time, and if the lock has expired, then set a new expiration time, return to 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 in this code? 1. Because it is the client's own generation expiration time, so it is necessary mandatory time for each client distributed must be synchronized. 2. When the lock expired time, if multiple clients simultaneously execute jedis.getSet()method, although it ultimately only one client can be locked, but the lock client expiration time may be covered by other clients. 3. The lock owner does not have the identity, that is, any client can be unlocked.

Unlock code

Correct posture

I would first display code, and then slowly bring you explain why this realization:

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;

    }

}

We can see that we need to unlock only two lines of code to get! The first line of code, we wrote a simple Lua script code, the last time to see this programming language, or in the "*** and painter" in, did not think this actually spend. The second line of code, we will Lua code spread jedis.eval()method, the parameters and KEYS [1] assigned lockKey, ARGV [1] assigned requestId. eval () method is Redis Lua code to perform the service side.

So what's the function of this Lua code is it? In fact, very simple, first get value corresponding to the value of the lock, check for equal requestId, if they are equal then remove the lock (unlock). So why use the Lua language to achieve it? To ensure that the operation was as atomic. About non-atomic would bring any problem, you can read the [unlock code - Error Example 2]. So why the implementation of eval () method ensures atomicity, due to the characteristics Redis, the following is a partial explanation of the official website of the eval command:

80% of people do not know the proper implementation of Redis Distributed Lock (Java version)

Simply put, when eval command Lua code, Lua code will be treated as a command to execute, and until the eval command is completed, Redis will execute other commands.

Error Example 1

The most common is the direct use unlock code jedis.del()method to remove the lock, this is not to determine the owner of the lock and unlock a direct way, will lead to any client can be unlocked at any time, even if the lock is not it.

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

Error Example 2

At first glance this unlock code is not the problem, even before I have almost achieved, and almost correct posture, the only difference is divided into two commands to execute code as follows:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {

    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}

Such as code comments, the problem is that if you call jedis.del()when the method when the lock does not belong to the current client will lift others plus the lock. So really there is such a scenario? The answer is yes, such as the client A lock, after a period of time the client A unlocked, performing jedis.del()before, suddenly lock expired, then the client attempts to lock B is successful, then the client A and then execute del () method, client B will lock to lifted.

to sum up

This article describes how to use Java code properly implemented Redis distributed lock for locking and unlocking were also given two more classic example of an error. In fact we want to achieve through a distributed lock Redis is not difficult, as long as the four conditions to ensure reliability in the meet. Although the Internet has brought us convenience, there is a problem as long as you can google, but the online answer must be right? In actual fact, we should always keep the skepticism, and more would like to verify.

If your project Redis is a multi-machine deployment, you can try Redissonto achieve a distributed lock, which is the official Java components provided by Redis.

Reproduced in: https: //blog.51cto.com/14214194/2411126

Guess you like

Origin blog.csdn.net/weixin_34221112/article/details/93033460