This blog uses the third-party open source component Jedis to implement the Redis client, and only consider the Redis server standalone deployment scenario.

preface

Distributed lock generally has three implementation methods: 1. Database optimistic lock; 2. 2. Distributed lock based on Redis; 3. Distributed lock based on ZooKeeper. This blog will introduce the second approach to distributed locking based on Redis. Although there are various blogs on the Internet that introduce Redis distributed lock implementation, however, their implementation has various problems. In order to avoid mistakes, this blog will explain how to correctly implement Redis distributed lock.


reliability

First, to ensure that distributed locks are available, we need to ensure that at least four of the following conditions are met:

  1. Mutual exclusivity. Only one client can hold the lock at any time.
  2. Deadlocks do not occur. Even if one client crashes while holding the lock and does not unlock actively, it is guaranteed that subsequent clients can lock it.
  3. It has fault tolerance. Clients can lock and unlock as long as most Redis nodes are running properly.
  4. You must tie the bell. Lock and unlock must be the same client, the client can not unlock the lock added by others.

Code implementation

The component dependence

First we’ll import the Jedis open source component via Maven. Add the following code to the POP.xml file:

Clients </groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>Copy the code

Lock code

Correct posture

Talk is cheap, show me the code. I’ll show you the code and explain why it’s done 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 obtain distributed lock * @param jedis Redis client * @param lockKey lock * @param requestId requestId * @param expireTime expiration time * @return */ 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; }}Copy the code

Jedis.set (String key, String value, String NXXX, String expx, int time);

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

  • The second value is value, we pass requestId, many children may not understand, there is a key as a lock is not enough, why use value? The reason is that when we talked about reliability above, the distributed lock must meet the fourth condition to unlock the bell. By assigning value to requestId, we can know which request added the lock, which can be based on when unlocking. The requestId can be generated using the uuID.randomuuid ().toString() method.

  • The third parameter is NXXX. We fill in NX for this parameter, which means SET IF NOT EXIST, that is, when key does NOT EXIST, we perform SET operation; If the key already exists, no operation is performed.

  • Expx = PX; expx = PX; expx = PX;

  • The fifth parameter is time, which corresponds to the fourth parameter and represents the expiration time of the key.

In general, the set() method above results in only two results: 1. There is no lock (key does not exist), then the lock is performed, and the lock is set to an expiration date. 2. No operation is performed on an existing lock.

As the careful child will see, our locking code meets the three conditions described in our reliability. First, set() takes an NX argument to ensure that the function will not be called if a key is already present, meaning that only one client can hold the lock, which is mutually exclusive. Second, because we set an expiration time on the lock, even if the lock owner subsequently crashes and does not unlock it, the lock will automatically unlock when it expires (i.e. the key is deleted) without deadlock. Finally, because we assign value to requestId, which represents the identity of the locked client request, we can verify that the client is the same client when it is unlocked. Since we only consider Redis single-machine deployment scenarios, we will not consider fault tolerance for the moment.

Error Example 1

A common example of an error is using a combination of jedis.setnx() and jedis.expire() to implement locking, as follows:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); If (result == 1) {// If (result == 1) {// If (result == 1) {// If (result == 1) {jedis.expire(lockKey, expireTime); }}Copy the code

The setnx() method is used to SET IF NOT EXIST, and the expire() method is used to add an expiration time to the lock. At first glance, this looks like the result of the previous set() method, but since these are two Redis commands and are not atomic, if the program crashes after executing setnx(), the lock is not set to expire. A deadlock will occur. This is being implemented online because the lower version of Jedis does not support the multi-parameter set() method.

Error Example 2

This type of error example is harder to detect and more complex to implement. Use the jedis.setnx() command to add a lock, where key is the lock, value is the expiration time of the lock. Execution procedure: 1. Use setnx() to attempt to lock. If the current lock does not exist, the system returns that the lock is successfully locked. 2. If the lock already exists, obtain the expiration time of the lock. Compared with the current time, if the lock has expired, set a new expiration time. 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 (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; }Copy the code

So what’s wrong with this code? 1. Because the client generates the expiration time, you must force the time of each client in distributed mode to be synchronized. 2. If multiple clients execute jedis.getSet() at the same time when a lock expires, only one client can be locked, but the expiration time of the lock on that client may be overwritten by other clients. 3. The lock does not have the owner identifier, that is, any client can unlock the lock.

The unlock code

Correct posture

I’ll show you the code and explain why it’s done this way:

public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * release distributed lock * @param jedis Redis client * @param lockKey lock * @param requestId requestId * @return release 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; }}Copy the code

As you can see, we only need two lines of code to unlock it! For the first line of code, we write a simple script for Lua, a programming language we last saw in Hackers and Painters. In the second line, we pass the Lua code to jedis.eval() and assign KEYS[1] to lockKey and ARGV[1] to requestId. The eval() method hands Lua code to the Redis server for execution.

So what does this Lua code do? If the value of the lock is equal to requestId, delete the lock (unlock). So why use Lua? Because you want to make sure that this is atomic. You can read unlock Code – Error Example 2 for more information about what non-atomicity can do. Eval () ensures atomicity. Eval () ensures atomicity.

Simply put, Lua code is executed as a command when eval is executed, and Redis does not execute any other commands until eval is executed.

Error Example 1

The most common unlock code is to use the jedis.del() method to remove the lock. This method unlocks the lock without determining who owns it, so that any client can unlock the lock at any time, even if it doesn’t own it.

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

Error Example 2

This unlocking code looks fine at first glance, and even I came close to doing it before. It’s similar to the correct pose, except that it’s split into two commands and the code looks like this:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {if (requestid.equals (jedis.get(lockKey)) {if (requestid.equals (jedis.get(lockKey))) { Jedis.del (lockKey); }}Copy the code

The problem, as noted in the code, 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, client A unlocks after A period of time, and before jedis.del() is executed, the lock suddenly expires, client B tries to lock successfully, and then client A executes del() to unlock client B.


conclusion

This paper mainly introduces how to use Java code to correctly implement Redis distributed lock, for lock and unlock are also given two more classic error examples. In fact, it is not difficult to implement distributed locks through Redis, as long as the four conditions of reliability are met. Although the Internet has brought us convenience, as long as there is a question can Google, but the online answer must be right? In fact, it is not, so we should always keep the spirit of questioning, want to verify more.

If you are working on a multi-machine Redis project, you can try using Redisson to implement distributed locking, which is a Java component of Redis, linked to in the References section.


Refer to the reading

[1] Distributed locks with Redis

[2] EVAL command

[3] Redisson