Using redis to implement a distributed lock service in Java

In modern programming languages, programmers who are exposed to programming with too many threads know more or less about locks.Simply put, locks in multiple threads are mechanisms that ensure the consistency of shared resources when multiple threads modify shared resources in a multithreaded environment.Don't expand here.In a distributed environment, the original multi-threaded locks are no longer needed, so there is a need for distributed locks.The so-called distributed lock service is a service that ensures the consistency of resources shared by multiple distributed services in a distributed environment.
Implementing a distributed lock service in a distributed environment is not easy, and many issues need to be considered for a single-process lock service.There are also many implementations of distributed locks.Here we discuss redis in Java.Open source implementations are already available in the redisson project in GitHub.But that's too complicated.Now let's implement a simple distributed lock service based on single-machine redis.This service must meet the following requirements

  • Supports immediate acquisition of locks or false acquisition if true or false acquisition is not available.
  • Supports wait for lock acquisition, if obtained, returns true directly, does not wait for a short period of time, repeatedly tries in this period of time, returns true if the attempt is successful, returns false if the wait time is not obtained after;
  • Deadlock cannot occur;
  • Unable to release locks that were not self-imposed;

Locking

Locking logic for distributed locks through redis is as follows:

Based on this logic, the core code to achieve locking is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if(jedis.exists(key)){
  jedis.set(key,value);
  jedis.expire(key,lockExpirseTime);
  return value;
}

It looks like this code is okay on the surface, but it doesn't actually perform the locking correctly in a distributed environment.To be able to correctly implement a lock operation, the three steps of determining whether a key exists, saving key-value, and setting the expiration time of a key must be atomic.If it is not an atomic operation, there are two possible scenarios:

  • After the "Decide whether the key exists" step yields a result that the key does not exist, before the "Save key-value" step, another client executes the same logic and performs the "Decide whether the key exists" step, yielding the same result that the key does not exist.This results in multiple clients acquiring the same lock.

  • After the client executes the Save key-value step, you need to set a key expiration time to prevent the client from deadlocking because the code quality is not unlocked or because the process crashes without unlocking.After the Save key-value step and before the Set Key Expiration Time step, the process may crash, causing the Set Key Expiration Time step to fail;

After redis version 2.6.12, the set command was expanded to avoid the two issues above.The parameters of the new redis set command are as follows

SET key value [EX seconds] [PX milliseconds] [NX|XX]

The new set command adds EX, PX, NX|XX parameter options.They mean the following

EX seconds - Sets the expiration time of the key in seconds per hour
 PX milliseconds - Sets the expiration time of the key in milliseconds
 NX - The value of the key is set only if the key does not exist
 XX - The value of the key is set only when the key exists

In this way, the original three-step operation can be completed in one set of atomic operations, avoiding the two questions mentioned above.The new redis lockout core code modifications are as follows:

jedis = redisConnection.getJedis();
jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
    return value;
}

The basic process of unlocking is as follows:

Based on this logic, the core code to unlock in Java is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
if(jedis.exists(key) && value.equals(jedis.get(key))){
    jedis.del(key);
    return true;
}
return false;

As with locking, the three steps of key existence, key holding, and key-value deletion need to be atomic. Otherwise, when a client finishes the "Determine whether or not they own the lock" step, it comes to the conclusion that they own the lock. When the expiration time of the lock expires, it is automatically released by redis, and another client is based on this key.Locking succeeds, and if the first client continues to delete the key-value, the lock that does not belong to him is released.This is obviously not working.Here we use redis'ability to execute Lua scripts to solve the problem of atomic operations.The modified unlock core code is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
    return true;
}

In addition, the mechanism for determining whether you own a lock is to use the key-value at the time of locking to determine whether the current key value is equal to the value you get when you hold a lock.Value must be a globally unique string when locking.

The complete code is as follows

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LockService;
import com.x9710.common.redis.RedisConnection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;

/**
 * Distributed Lock redis Implementation
 *
 * @author Yang Gaochao
 * @since 2017-12-14
 */
public class LockServiceRedisImpl implements LockService {

  private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);

  private static String SET_SUCCESS = "OK";

  private static String KEY_PRE = "REDIS_LOCK_";

  private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");

  private RedisConnection redisConnection;

  private Integer dbIndex;

  private Integer lockExpirseTime;

  private Integer tryExpirseTime;

  public void setRedisConnection(RedisConnection redisConnection) {
      this.redisConnection = redisConnection;
  }

  public void setDbIndex(Integer dbIndex) {
      this.dbIndex = dbIndex;
  }

  public void setLockExpirseTime(Integer lockExpirseTime) {
      this.lockExpirseTime = lockExpirseTime;
  }

  public void setTryExpirseTime(Integer tryExpirseTime) {
      this.tryExpirseTime = tryExpirseTime;
  }

  public String lock(String key) {
      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String value = fetchLockValue();
          if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
              log.debug("Reids Lock key : " + key + ",value : " + value);
              return value;
          }
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return null;
  }

  public String tryLock(String key) {

      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String value = fetchLockValue();
          Long firstTryTime = new Date().getTime();
          do {
              if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
                  log.debug("Reids Lock key : " + key + ",value : " + value);
                  return value;
              }
              log.info("Redis lock failure,waiting try next");
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return null;
  }

  public boolean unLock(String key, String value) {
      Long RELEASE_SUCCESS = 1L;
      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
          if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
              return true;
          }
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return false;
  }

/**
 * Generate Locked Unique String
 *
 * @return Unique string
 */
  private String fetchLockValue() {
      return UUID.randomUUID().toString() + "_" + df.format(new Date());
  }
}

Test Code

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.LockServiceRedisImpl;

public class RedisLockTest {

  public static void main(String[] args) {
      for (int i = 0; i < 9; i++) {
          new Thread(new Runnable() {
              public void run() {
                  RedisConnection redisConnection = RedisConnectionUtil.create();
                  LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();
                  lockServiceRedis.setRedisConnection(redisConnection);
                  lockServiceRedis.setDbIndex(15);
                  lockServiceRedis.setLockExpirseTime(20);
                  String key = "20171228";
                  String value = lockServiceRedis.lock(key);
                  try {
                      if (value != null) {
                          System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");
                          Thread.sleep(25 * 1000);
                      }else{
                          System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      if (value == null) {
                          value = "";
                      }
                      System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));

                  }
              }
          }).start();
      }
  }
}

test result

Thread-1 lock key = 20171228 failure! 
Thread-2 lock key = 20171228 failure! 
Thread-4 lock key = 20171228 failure! 
Thread-8 lock key = 20171228 failure! 
Thread-7 lock key = 20171228 failure! 
Thread-3 lock key = 20171228 failure! 
Thread-5 lock key = 20171228 failure! 
Thread-0 lock key = 20171228 failure! 
Thread-6 lock key = 20171228 success! 
Thread-1 unlock key = 20171228 false
Thread-2 unlock key = 20171228 false
Thread-4 unlock key = 20171228 false
Thread-8 unlock key = 20171228 false
Thread-3 unlock key = 20171228 false
Thread-5 unlock key = 20171228 false
Thread-0 unlock key = 20171228 false
Thread-7 unlock key = 20171228 false
Thread-6 unlock key = 20171228 true

From the test results, we can see that nine threads simultaneously lock one key, only one can successfully acquire the lock, and the rest of the clients cannot acquire the lock.

Postnote

This code also implements a tryLock interface.This is mainly because when the client is unable to acquire the lock, it repeatedly attempts to acquire the lock over a short period of time.

Write at the end:

Welcome to my public number, there are a lot of Java related articles, learning materials will be updated and sorted data will be put in it.

If you think the writing is good, give a compliment and pay attention to it!Pay attention, don't get lost, keep updating!!!

Tags: Java Jedis Redis Programming

Posted on Tue, 16 Jun 2020 13:17:31 -0400 by plasmahba