There are generally three ways to implement distributed locks: 1. Optimistic database lock; 2. Distributed lock based on Redis; 3. Distributed lock based on ZooKeeper. This blog will introduce the second method, which is to implement distributed locking based on Redis. Although there are various blogs on the Internet that introduce the implementation of Redis distributed locks, their implementation has various problems. In order to avoid misleading children, this blog will introduce in detail how to correctly implement Redis distributed locks.
reliability
First, in order to ensure that distributed locks are available, we must at least ensure that the implementation of the lock meets the following four conditions at the same time:
Mutual Exclusion. At any time, only one client can hold the lock.
No deadlock occurs. Even if a client crashes during the lock holding period without actively unlocking it, it can ensure that other clients can add locks.
Fault-tolerant. As long as most Redis nodes are running normally, the client can lock and unlock.
The person who tied the bell must be untied. Locking and unlocking must be the same client, and the client cannot untie the locks added by others.
Code implementation
Component dependencies
First, we need to introduce Jedis open source components 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>
Lock code
Correct posture
Talk is cheap, show me the code. First show the code and then explain why this is implemented:
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";/** * Try to obtain distributed lock* @param jedis Redis client* @param lockKey lock* @param requestId Request ID* @param expireTime Expiry time* @return Whether it is successfully obtained*/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 you can see, we just add a single line of code: jedis.set(Stringkey,Stringvalue,Stringnxxx,Stringexpx,inttime). This set() method has five formal parameters in total:
The first one is a key, we use the key as the lock, because the key is unique.
The second one is value. What we are passing on is requestId. Many children's shoes may not understand. Isn't it enough to have a key as a lock? Why do we still need to use value? The reason is that when we talked about reliability above, the distributed lock must meet the fourth condition to unzip the bell. By assigning the value to the requestId, we will know which request was added to the lock, and there will be a basis for unlocking. requestId can be generated using the UUID.randomUUID().toString() method.
The third one is nxxx. We fill in this parameter NX, which means SETIFNOTEXIST, that is, when the key does not exist, we perform the set operation; if the key already exists, no operation is done;
The fourth one is expx. We pass this parameter PX, which means that we want to add an expired setting to this key. The specific time is determined by the fifth parameter.
The fifth is time, which echoes the fourth parameter and represents the expiration time of the key.
In general, executing the set() method above will only lead to two results: 1. There is no lock at present (key does not exist), then the lock operation is performed and the lock is set to be valid for the lock, and the value represents the locked client. 2. There is a lock already exists and no operation is done.
If you are careful, you will find that our lock code meets the three conditions described in our reliability. First of all, set() adds NX parameters, which can ensure that if a key already exists, the function will not be called successfully, that is, only one client can hold the lock to satisfy mutex. Secondly, since we set an expiration time for the lock, even if the lock holder crashes in the subsequent crash and does not unlock, the lock will automatically be unlocked because it has reached the expiration time (that is, the key is deleted), and there will be no deadlock. Finally, because we assign value to requestId, which represents the locked client request identity, then the client can verify whether it is the same client when unlocking. Since we only consider Redis stand-alone deployment scenarios, we will not consider fault tolerance for the time being.
Error Example 1
A common error example is to use the combination of jedis.setnx() and jedis.expire() to achieve locking. The 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) {// If the program suddenly crashes here, the expiration time cannot be set, and a deadlock will occur jedis.expire(lockKey, expireTime);}}The function of the setnx() method 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. However, since these are two Redis commands, they are not atomic. If the program suddenly crashes after executing setnx(), the lock does not set the expiration time. Then a deadlock will occur. The reason why some people implement this on the Internet is that the lower version of Jedis does not support the multi-parameter set() method.
Error Example 2
This error example is more difficult to find problems, and the implementation is also more complicated. Implementation idea: Use the jedis.setnx() command to implement locking, where key is the lock and value is the expiration time of the lock. Execution process: 1. Try to add a lock through the setnx() method. If the current lock does not exist, the lock will be successfully returned. 2. If the lock already exists, acquire 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 locking is successfully added. 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, the lock is successfully returned if (jedis.setnx(lockKey, expiresStr) == 1) {return true;}// If the lock exists, the expiration time of the lock is obtained String currentValueStr = jedis.get(lockKey);if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) {// The lock has expired, obtain the expiration time of the previous lock, and set the expiration time of the current lock String oldValueStr = jedis.getSet(lockKey, expiresStr);if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {// Considering the case of multi-thread concurrency, only if the setting value of one thread is the same as the current value, it has the right to lock return true;}}// In other cases, the lock failure will be returned return false;}So what's the problem with this code? 1. Since the client generates the expiration time itself, it is necessary to force the time of each client to be synchronized under the distributed approach. 2. When the lock expires, if multiple clients execute the jedis.getSet() method at the same time, although only one client can lock in the end, the expiration time of the lock of this client may be overwritten by other clients. 3. The lock does not have the owner logo, that is, any client can unlock it.
Unlock code
Correct posture
Let’s show the code first, and then slowly explain why this is implemented:
public class RedisTool {private static final long RELEASE_SUCCESS = 1L;/** * Release the distributed lock* @param jedis Redis client* @param lockKey lock* @param requestId Request ID* @return Whether the release was 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;}}As you can see, we only need two lines of code to unlock it! In the first line of code, we wrote a simple Lua script code. The last time we saw this programming language was in "Hacker and Painter", but we didn't expect it to be used this time. In the second line of code, we pass the Lua code to the jedis.eval() method, and assign the parameter KEYS[1] to lockKey and ARGV[1] to requestId. The eval() method is to hand over the Lua code to the Redis server for execution.
So what is the function of this Lua code? In fact, it is very simple. First, get the value corresponding to the lock, check whether it is equal to requestId, and if it is equal, delete the lock (unlock). So why use Lua language to implement it? Because it is necessary to ensure that the above operations are atomic. For what problems atomicity will bring, you can read [Unlock Code - Error Example 2]. So why can I execute the eval() method ensure atomicity, which originates from the characteristics of Redis. Here is a partial explanation of the eval command on the official website:
Simply put, when the eval command executes Lua code, the Lua code will be executed as a command, and Redis will not execute other commands until the eval command is executed.
Error Example 1
The most common unlocking code is to directly use the jedis.del() method to delete the lock. This method of unlocking directly without first judging the owner of the lock will cause any client to unlock at any time, even if the lock is not its.
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); }Error Example 2
At first glance, this unlocking code is fine. I even almost implemented it like this before, which is similar to the correct posture. The only difference is that it is divided into two commands to execute. The code is as follows:
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {// Determine whether the locking and unlocking are the same client if (requestId.equals(jedis.get(lockKey)))) {// If this lock is suddenly not from this client, it will be misunderstood to unlock jedis.del(lockKey);}}As for code comments, the problem is that if the jedis.del() method is called, the lock will be unlocked when it no longer belongs to the current client. So is there really such a scenario? The answer is yes. 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, and then client B's lock is unlocked.
Summarize
This article mainly introduces how to correctly implement Redis distributed lock using Java code. Two classic error examples are given for locking and unlocking. In fact, it is not difficult to implement distributed locks through Redis, as long as it is guaranteed to meet the four conditions in reliability.
In what scenario are distributed locks mainly used? Where synchronization is required, for example, inserting a piece of data requires checking in advance whether the database has similar data. When multiple requests are inserted at the same time, it may be determined that the database has no similar data, and all of them can be added. At this time, synchronous processing is required, but the direct database locking table is too time-consuming, so Redis distributed lock is used. At the same time, only one thread can perform the operation of inserting data, and other threads are waiting.
The above is all the content of this article about the Java language describing the correct implementation of Redis distributed locks. I hope it will be helpful to everyone. Interested friends can continue to refer to other related topics on this site. If there are any shortcomings, please leave a message to point it out. Thank you friends for your support for this site!