In modern programming languages, programmers who have been exposed to multithreaded programming know more or less about locking. Simply put, the lock in multithreading is a mechanism to ensure the consistency of shared resources when multiple threads modify them in a multi-threaded environment. I’m not going to expand it. In the distributed environment, the original multi-threaded locking does not work, and there is a need for distributed locking. The so-called distributed lock service is to ensure the consistency of resources shared by multiple distributed services in a distributed environment.

It is not easy to implement a distributed lock service in a distributed environment. There are many issues that need to be considered in a single process lock service. There are also many implementations of distributed locking. Here we’re talking about redis in Java. There is already an open source implementation in the Redisson project on GitHub. But that’s too complicated. Now let’s implement a simple distributed locking service based on stand-alone Redis. This service must meet the following requirements

  1. Return true if the lock is obtained, false if it is not.
  2. If the lock is not acquired, return true for a short period of time. If the attempt is successful, return true. If the lock is not acquired after the wait time, return false.
  3. Cannot generate deadlock condition;
  4. Cannot release locks not created by oneself.

Below we use an example to demonstrate the use of Redis in Java to implement the distributed locking service

# # lock

Redis is used to implement the locking logic of distributed locks as follows:

Based on this logic, the core code for implementing the lock 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;
}
Copy the code

On the face of it, this code doesn’t seem to be a problem, but it doesn’t actually implement locking correctly in a distributed environment. To correctly implement the locking operation, the three steps of determining whether the key exists, saving the key-value, and setting the expiration time of the key must be atomic operations. If the operation is not atomic, then one of two things can happen:

  1. After the result step of “determine whether the key exists” shows that the key does not exist, before the “save key-value” step, another client performs the same logic and executes the “determine whether the key exists” step, which also shows that the key does not exist. This results in multiple clients acquiring the same lock;
  2. After the “Save key-value” step is executed on the client, you need to set the expiration time of a key to prevent the client from being locked due to code quality or process crash. The process may crash after the Save key-value step but before the Set key expiration step. As a result, the Set key expiration step fails.

Redis has expanded the set command since version 2.6.12 to avoid both of these problems. The new version of the redis set command takes the following parameters

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

The new version of the set command adds the EX, PX, NX | XX parameter options. Here’s what they mean

EX milliseconds - Set the number of milliseconds the key will expire, in PX milliseconds - Set the number of milliseconds the key will expire. The unit is milliseconds NX - the key value is set only if the key does not exist - the key value is set only if the key does existCopy the code

In this way, the original three steps can be done in a set atomic operation, avoiding the two problems mentioned above. The new redis locking core code changes 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;
}
Copy the code

The basic process of unlocking is as follows:

Based on this logic, the core code for unlocking in Java looks like this:

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

As with locking, the three steps of whether the key exists, determining whether it owns the lock, and ** deleting the key-value ** need to be atomic operations. Otherwise, when a client completes the step of “determining whether it owns the lock”, it concludes that it owns the lock. At this time, the lock expires and is automatically released by Redis. At the same time, another client successfully locks the key based on the key. If the first client continues to delete the key-value, the lock that is not its own will be released. This is obviously not going to work. Here we take advantage of 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;
}
Copy the code

In addition, the mechanism for determining whether you own the lock is to use the key-value of the lock to determine whether the current key value is equal to the value obtained when you own the lock. The value of the lock must be a globally unique string.

The complete code is shown below

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; /** * redis implements ** @author * @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 unique String with lock ** @return unique String */ private String fetchLockValue() {return uuid.randomUuid ().toString() + "_" + df.format(new Date()); }}Copy the code

## 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(); }}}Copy the code

The test results

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
Copy the code

From the test results, we can see that nine threads lock a key at the same time, only one can successfully acquire the lock, the rest of the client can not acquire the lock.

This code also implements a tryLock interface. This is primarily the case where the client is unable to acquire a lock and repeatedly tries to acquire it over a short period of time.

This program is done by adding new implementation classes to the previous article “Using Redis in Java.” The code is published synchronously in the GitHub repository

The original was published in a brief book,The original link