Distributed lock based on Redis distributed lock

1. Brief introduction

       There are generally three implementations of distributed locks: first, database optimistic locks; second, distributed locks based on Redis; third, distributed locks based on Zookeeper. At present, there are scenarios in which distributed locks are needed in the project, so I learned and summarized them. Today, let's talk about distributed locks based on Redis.

       To ensure the availability of distributed locks based on Redis, the following four conditions must be met at the same time: 1. Mutual exclusion: only one client can hold the lock at any time; 2. Avoid deadlock: even if one client is holding the lock If there is a crash in the lock phase without actively releasing the lock, it is also necessary to ensure that other clients can lock in the follow-up; 3. Fault tolerance: as long as most Redis nodes can run normally, the client can lock and unlock; 4. Uniqueness : During the process of locking and unlocking, the client must only be the same client, and the client cannot unlock other clients by itself. The above four points are called reliability.

 

2. Simple example

1. maven dependencies

<properties>
    <jedis.version>2.9.0</jedis.version>
</properties>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${jedis.version}</version>
</dependency>

 2. RedisTool tool class

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";
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * Attempt to acquire a distributed lock
     *
     * @param jedis Redis client
     * @param lockKey lock
     * @param requestId request ID
     * @param expireTime timeout
     * @return
     */
    public static boolean tryGetLock(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;
    }

    /**
     * Release distributed lock
     *
     * @param jedis Jedis client
     * @param lockKey lock
     * @param requestId request ID
     * @return
     */
    public static boolean releaseLock(Jedis jedis, String lockKey, String requestId){
         String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
         Object object = jedis.eval(luaScript);
         if (RELEASE_SUCCESS.equals(object)) {
            return true;
         }
         return false;
    }
}

       As can be seen from the locking method, the locking is just one line of code: jedis.set(String key, String value, String nxxx, String expx, int time), this set method has a total of five parameters:

  • key: Use the key as the lock because the key is unique.
  • value: What we are requesting is requestId. The reason for this parameter is that it is based on the fourth point of reliability. In other words, the ringer must be connected to unlock the bell. Generally speaking, we will generate requestId through UUID.randomUUID().toString().
  • nxxx: This parameter requests NX, which is SET IF NOT EXIST. When the key does not exist, perform the set operation; if the key already exists, do nothing.
  • expx: The request is PX, which is to set an expiration time for the key. The specific time is determined by the parameter time.

There are only two results of the above code execution: 1. If there is no lock at present, the lock operation is performed, and the expiration time of the lock is set, and the value is set to the locked client; 2. The lock already exists, and no operation is performed.

 

Three, lock example analysis

        If you read the previous content carefully, you will find that the above example does not meet the fault tolerance in reliability. This is because fault tolerance is a factor that needs to be considered in the environment of redis cluster. When deploying redis on a single machine, the priority of fault tolerance is the lowest.

        In reading the code written by other small partners in the team about redis implementing distributed locks, the following two wrong implementations were found, as follows:

  • Error example one
public static void tryGetLockWithWrong(Jedis jedis, String lockKey, String requestId, int expireTime){
         Long result = jedis.setnx(lockKey, requestId);
         if (result == 1) {
             jedis.expire(lockKey, expireTime);
         }
    }

The function of setnx() is SET IF NOT EXIST, and the expire() method is to add an expiration time to the lock. However, since these two redis commands are not atomic, if jedis.expire(String key, int expireTime) is abnormal or crashes, the lock expiration time set by this method will not take effect, which will lead to deadlock. The source code snippet of expire is as follows:

//Jedis.java
public Long expire(final String key, final int seconds) {
    checkIsInMultiOrPipeline();
    client.expire(key, seconds);
    return client.getIntegerReply();
}

//Client.java
public void expire(final String key, final int seconds) {
    expire(SafeEncoder.encode(key), seconds);
}

//BinaryClient.java
public void expire(final byte[] key, final int seconds) {
    sendCommand(EXPIRE, key, toByteArray(seconds));
}

//Connection.java
protected Connection sendCommand(final Command cmd, final byte[]... args) {
    try {
      connect();
      Protocol.sendCommand(outputStream, cmd, args);
      pipelinedCommands++;
      return this;
    } catch (JedisConnectionException ex) {
      try {
        String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
        if (errorMessage != null && errorMessage.length() > 0) {
          ex = new JedisConnectionException(errorMessage, ex.getCause());
        }
      } catch (Exception e) {
      }
      // Any other exceptions related to connection?
      broken = true;
      throw ex;
    }
  }

 The reason for this writing is that the lower version of jedis does not support the multi-parameter set method.

 

  • Error example two
public static boolean getLockWithWrong(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 implementation idea of ​​this code: use the jedis.setnx() command to implement locking, 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.

       At first glance, there is no problem with this logic. After careful analysis, there are the following problems: 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.

 

Fourth, unlock example analysis

       It can be seen from the unlocking method that unlocking only requires two lines of code: the first line of code, a simple Lua script code; the second line of code, the Lua code is passed to the jedis.eval() method, and the parameter KEYS [1] is assigned as lockKey, and ARGV[1] is assigned as requestId. The eval() method is to hand the Lua code to the Redis server for execution.

       The introduction of the lua script is only to ensure the atomicity of the unlocking operation (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). The logic 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.

       When reading the unlocking method of small partners in the team, there are also two wrong implementation methods, which are as follows:

  • Error example one
public static void releaseLockWithWrong(Jedis jedis, String lockKey) {
      jedis.del(lockKey);
}

       Delete the lock directly using the jedis.del() method. This method of unlocking the lock without first determining the owner of the lock will cause any client to unlock it at any time, even if the lock is not its own.

  • Error example two
public static void wrongReleaseLock(Jedis jedis, String lockKey, String requestId) {
        if (requestId.equals(jedis.get(lockKey))) {
            jedis.del(lockKey);
        }
    }

       The problem is that if the jedis.del() method is called, the locks added by others will be released when the lock no longer belongs to the current client. For example, client A locks, and after a period of time, client A unlocks. Before executing jedis.del(), the lock suddenly expires. At this time, client B tries to lock successfully, and then client A executes the del() method again. Then the lock of client B is released.

 

V. Summary

       When implementing distributed locks based on Redis, you should look and think more, instead of blindly looking for them on the Internet.

Guess you like

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