English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Correct Implementation of Redis Distributed Locks in Java Language

There are generally three ways to implement distributed locks:1. Database optimistic lock;2. Based on Redis's distributed lock;3. Based on ZooKeeper's distributed lock. This blog will introduce the second method, which is the distributed lock based on Redis. Although there are various blogs on the Internet introducing the implementation of Redis distributed locks, their implementations have various problems. To avoid misleading readers, this blog will introduce in detail how to correctly implement Redis distributed locks.

Reliability

Firstly, to ensure that the distributed lock is available, we must at least ensure that the lock implementation meets the following four conditions simultaneously:

Mutual exclusion. At any given time, only one client can hold the lock.

Deadlock will not occur. Even if a client crashes while holding the lock without actively unlocking, it can ensure that subsequent other clients can lock.

Fault-tolerant. As long as most of the Redis nodes are running normally, the client can lock and unlock.

To untie the bell, you must tie it back. Locking and unlocking must be performed by the same client, and the client cannot unlock the lock added by others.

Code implementation

Component dependency

Firstly, we need to introduce the Jedis open-source component through Maven, and add the following code to the pom.xml file:

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

Locking code

Correct posture

Talk is cheap, show me the code. First show the code, then explain why it is implemented in this way:

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";
	/** 
   * Attempt to acquire a distributed lock 
   * @param jedis Redis client 
   * @param lockKey Lock 
   * @param requestId Request identifier 
   * @param expireTime Timeout time 
   * @return Whether the acquisition is successful 
   */
	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 can be seen, we lock with just one line of code: jedis.set(String key, String value, String nxxx, String expx, int time), and this set() method has five formal parameters:

The first one is key, we use key as the lock because key is unique.

The second one is value, we pass requestId, and many students may not understand why we need to use value when a key as a lock is enough. The reason is that when we discussed reliability above, the distributed lock must meet the fourth condition that the bell must be untied by the person who tied it. By assigning the value as requestId, we know which request locked this lock, and we can have a basis for unlocking. requestId can be generated using the UUID.randomUUID().toString() method.

The third one is nxxx, this parameter we fill in is NX, meaning SETIFNOTEXIST, which means that we perform the set operation when the key does not exist; if the key already exists, no operation is performed;

The fourth one is expx, this parameter we pass is PX, meaning we want to add an expiration setting to this key, and the specific time is determined by the fifth parameter.

The fifth one is time, corresponding to the fourth parameter, representing the expiration time of the key.

In summary, executing the above set() method will only result in two outcomes:1.If there is no lock (the key does not exist), then perform the locking operation, set an expiration time for the lock, and the value represents the client that locked it.2.If a lock already exists, no operation is performed.

The meticulous students will notice that our locking code meets the three conditions described in our reliability. First, the set() method includes the NX parameter, which ensures that if a key already exists, the function will not succeed, meaning that only one client can hold the lock, satisfying mutual exclusion. Secondly, since we have set an expiration time for the lock, even if the lock holder crashes without unlocking, the lock will automatically unlock (i.e., the key is deleted) due to the expiration time, preventing deadlock. Finally, because we assign the value as requestId, representing the request identifier of the client that locked it, the client can verify whether it is the same client when unlocking. Since we only consider the single-machine deployment scenario of Redis, we do not consider fault tolerance for the time being.

Error Example1

A common error example is to use the combination of jedis.setnx() and jedis.expire() to implement locking, as shown in the code below:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
	long result = jedis.setnx(lockKey, requestId);
	if (result == 1) {
		// If the program crashes here, the expiration time cannot be set, and a deadlock will occur. 
		jedis.expire(lockKey, expireTime);
	}
}

The setnx() method's function is SETIFNOTEXIST, and the expire() method is to add an expiration time to the lock. At first glance, it seems to be the same as the previous set() method, but since these are two Redis commands, they do not have atomicity. If the program crashes after executing setnx(), and the lock does not have an expiration time set, a deadlock will occur. Some people implement it like this on the Internet because the low version of jedis does not support the multi-parameter set() method.

Error Example2

This type of error example is relatively difficult to find and the implementation is also complex. Implementation idea: Use the jedis.setnx() command to implement locking, where the key is the lock and the value is the lock's expiration time. Execution process:1.Attempt to lock using the setnx() method. If the current lock does not exist, return success for the lock acquisition.2.If the lock already exists, get the lock's expiration time and compare it with the current time. If the lock has expired, set a new expiration time and return success for the lock acquisition. 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 the current lock does not exist, return lock acquisition success 
	if (jedis.setnx(lockKey, expiresStr) == 1) {
		return true;
	}
	// If the lock exists, get the lock's expiration time 
	String currentValueStr = jedis.get(lockKey);
	if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) {
		// The lock has expired, get the expiration time of the previous lock and set the current lock's expiration time 
		String oldValueStr = jedis.getSet(lockKey, expiresStr);
		if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
			// Considering the case of multi-threaded concurrency, only when a thread's set value is the same as the current value can it have the right to lock 
			return true;
		}
	}
	// In other cases, return lock acquisition failure by default 
	return false;
}

Then where is the problem with this code?1.Since the expiration time is generated by the client itself, it is necessary to strictly require that the time of each client in the distributed system be synchronized.2.When the lock expires, if multiple clients simultaneously execute the jedis.getSet() method, although only one client can lock in the end, the expiration time of this client's lock may be overwritten by other clients.3.The lock does not have an owner identifier, meaning any client can unlock it.

Unlock code

Correct posture

Let's show the code first, and then explain slowly why it is implemented this way:

public class RedisTool {
	private static final long RELEASE_SUCCESS = 1L;
	/** 
   * Release distributed lock 
   * @param jedis Redis client 
   * @param lockKey Lock 
   * @param requestId Request identifier 
   * @return Whether the release is successful 
   */
	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;
	}
}

It can be seen that we only need two lines of code to unlock! The first line of code, we wrote a simple Lua script, the last time we saw this programming language was in 'Hackers & Painters', and we didn't expect to use it this time. The second line of code, we pass the Lua code to the jedis.eval() method and make the parameter KEYS[1Set to lockKey, ARGV[1Set to requestId. The eval() method is to hand over the Lua code to the Redis server for execution.

What is the function of this Lua code? It's actually very simple. First, it gets the value corresponding to the lock, checks if it is equal to requestId, and if it is, deletes the lock (unlocks it). Why use Lua language to implement it? Because it ensures that the above operation is atomic. For what problems non-atomicity can cause, you can read [Unlocking Code-Error Example2Then why can executing the eval() method ensure atomicity? This is due to the characteristics of Redis, and here is a partial explanation of the eval command from the official website:

In simple terms, when the eval command executes Lua code, the Lua code is executed as a command, and Redis will not execute other commands until the eval command is completed.

Error Example1

The most common unlocking code is to directly use the jedis.del() method to delete the lock. This way of unlocking without first checking the lock owner can lead to any client being able to unlock the lock at any time, even if it is not theirs.

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

Error Example2

This unlocking code appears to be fine at first glance, and I almost implemented it that way before, quite similar to the correct approach, the only difference being that it is executed in two commands, as shown below:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
	// Determine whether the locking and unlocking are from the same client 
	if (requestId.equals(jedis.get(lockKey))) {
		// If at this time, this lock is not this client's, it will misunderstand the lock 
		jedis.del(lockKey);
	}
}

As indicated by the code comments, the problem lies in the case where the lock does not belong to the current client when calling the jedis.del() method, the lock added by others will be released. Is there really such a scenario? The answer is yes, for example, client A adds a lock, 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, then the lock of client B is released.

Summary

This article mainly introduces how to correctly implement Redis distributed lock with Java code, and also gives two classic error examples for locking and unlocking. In fact, it is not difficult to implement distributed lock through Redis, as long as it can meet the four conditions of reliability.

What scenarios are distributed locks mainly used in? Synchronized places, such as inserting a piece of data, need to check in advance whether there is similar data in the database, and multiple requests insert at the same time. It may be judged that the database returns no similar data, so all can join. At this time, synchronous processing is required, but directly locking the database table is too time-consuming, so Redis distributed lock is adopted, and only one thread can perform the operation of inserting data at the same time, and other threads wait.

That's all about the correct implementation of Redis distributed lock for Java language description in this article. I hope it will be helpful to everyone. Those who are interested can continue to read other related topics on this site. Welcome to leave a message if there is anything insufficient. Thank you for your support to this site!

Declaration: The content of this article is from the Internet, the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously. This website does not own the copyright, has not been manually edited, and does not assume any relevant legal liability. If you find any content suspected of copyright infringement, please send an email to: notice#w3Please report via email to codebox.com (replace # with @ when sending email) and provide relevant evidence. Once verified, this site will immediately delete the content suspected of infringement.

You May Also Like