preface

Distributed locks are generally implemented in three ways:

1. Optimistic database lock;

2. Distributed lock based on Redis;

3. Distributed lock based on ZooKeeper.

This technical article will introduce a second approach to distributed locking based on Redis. Although there are various blogs on the web that introduce Redis distributed lock implementation, their implementation has various problems. In order to avoid misunderstanding, this technical article will explain how to implement Redis distributed lock correctly.

What is distributed lock?

To introduce distributed locks, we must first mention that the corresponding distributed locks are thread locks and process locks.

Thread lock

Thread lock: used to lock methods and code blocks. When a method or code uses a lock, only one thread executes the method or code segment at a time. Thread locks are only effective within the same JVM, because implementation of thread locks is fundamentally based on shared memory between threads, such as synchronized, which is a shared object header, and Lock, which is a shared variable (state).

Process the lock

Process lock: To control the access of multiple processes in the same operating system to a shared resource, processes are independent and cannot access resources of other processes. Therefore, thread locks such as synchronized cannot be used to implement process locks.

A distributed lock

Distributed lock: When multiple processes are not in the same system, a distributed lock is used to control the access of multiple processes to resources.

Second, the use scenario of distributed lock

Both interthread and interprocess concurrency problems can be solved by distributed locking, but this is strongly discouraged! Because solving these small problems with distributed locks is very resource-intensive! Distributed locks are most appropriate for solving the problem of multi-process concurrency in distributed situations.

There is A situation where thread A and thread B both share some variable X.

In the case of a single machine (single JVM), where memory is shared between threads, the concurrency problem can be solved by simply using thread locks.

In distributed (multi-JVM) situations, thread A and thread B are most likely not in the same JVM, and thus the thread lock will not work, so the distributed lock will be used to solve the problem.

Three, distributed lock implementation (Redis)

The key to realize distributed lock is to build a storage server outside the distributed application server to store lock information. At this time, we can easily think of Redis. First we need to build a Redis server, with Redis server to store lock information.

Key Points to Note

A few key points to note when implementing:

1, lock information must be expired timeout, can not let a thread hold a lock for a long time to cause deadlock;

2. Only one thread can acquire the lock at a time.

A few redis commands to use:

Setnx (key, value) : “Set if not exits”. If the key-value does not exist, it is successfully added to the cache and returns 1. Otherwise, 0 is returned.

Get (key) : Gets the value of the key, or returns nil if none exists.

Getset (key, value) : gets the value of the key, returns nil if it does not exist, and then updates the old value to the new value.

Expire (key, seconds) : Sets the validity period of key-value to seconds.

Take a look at the flow chart:

Under this process, no deadlock is caused.

I use Jedis as the Redis client API, the following is to see the specific implementation of the code.

(1) First create a Redis connection pool.

public class RedisPool { private static JedisPool pool; Private static int maxTotal = 20; Private static int maxIdle = 10; Private static int minIdle = 5; Private static Boolean testOnBorrow = true; Private static Boolean testOnReturn = false; Static {initPool(); } public static Jedis getJedis(){return pool.getResource(); } public static void close(Jedis jedis){ jedis.close(); } private static void initPool(){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxTotal); config.setMaxIdle(maxIdle); config.setMinIdle(minIdle); config.setTestOnBorrow(testOnBorrow); config.setTestOnReturn(testOnReturn); config.setBlockWhenExhausted(true); Pool = new JedisPool(config, "127.0.0.1", 6379, 5000, "liqiyao"); }}Copy the code

(2) Encapsulate Jedis API to encapsulate some operations needed to achieve distributed lock.

public class RedisPoolUtil { private RedisPoolUtil(){} private static RedisPool redisPool; public static String get(String key){ Jedis jedis = null; String result = null; try { jedis = RedisPool.getJedis(); result = jedis.get(key); } catch (Exception e){ e.printStackTrace(); } finally { if (jedis ! = null) { jedis.close(); } return result; } } public static Long setnx(String key, String value){ Jedis jedis = null; Long result = null; try { jedis = RedisPool.getJedis(); result = jedis.setnx(key, value); } catch (Exception e){ e.printStackTrace(); } finally { if (jedis ! = null) { jedis.close(); } return result; } } public static String getSet(String key, String value){ Jedis jedis = null; String result = null; try { jedis = RedisPool.getJedis(); result = jedis.getSet(key, value); } catch (Exception e){ e.printStackTrace(); } finally { if (jedis ! = null) { jedis.close(); } return result; } } public static Long expire(String key, int seconds){ Jedis jedis = null; Long result = null; try { jedis = RedisPool.getJedis(); result = jedis.expire(key, seconds); } catch (Exception e){ e.printStackTrace(); } finally { if (jedis ! = null) { jedis.close(); } return result; } } public static Long del(String key){ Jedis jedis = null; Long result = null; try { jedis = RedisPool.getJedis(); result = jedis.del(key); } catch (Exception e){ e.printStackTrace(); } finally { if (jedis ! = null) { jedis.close(); } return result; }}}Copy the code

(3) Distributed lock tool class

public class DistributedLockUtil { private DistributedLockUtil(){ } public static boolean lock(String System.out.println(thread.currentThread () + "Start trying to lock!" ); Long result = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000)); if (result ! = null && result.intValue() == 1){system.out.println (thread.currentThread () + "lock"! ); RedisPoolUtil.expire(lockName, 5); System.out.println(thread.currentThread () + "Execute business logic!" ); RedisPoolUtil.del(lockName); return true; } else { String lockValueA = RedisPoolUtil.get(lockName); if (lockValueA ! = null && Long.parseLong(lockValueA) >= System.currentTimeMillis()){ String lockValueB = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + 5000)); If (lockValueB = = null | | lockValueB. Equals (lockValueA)) {System. Out. Println (Thread. The currentThread () + "locking success!" ); RedisPoolUtil.expire(lockName, 5); System.out.println(thread.currentThread () + "Execute business logic!" ); RedisPoolUtil.del(lockName); return true; } else { return false; } } else { return false; }}}}Copy the code

reliability

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

  1. ** mutually exclusive. ** Only one client can hold the lock at any time.
  2. ** no deadlocks occur. ** Even if a client crashes while holding the lock and does not unlock it actively, it is guaranteed that subsequent clients can lock it.
  3. ** has fault tolerance. ** Clients can lock and unlock as long as most Redis nodes are running properly.
  4. He who will settle the bell must tie it. ** 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.

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.

Xiaobian share content is over here!

Redis distributed locks and the latest Java core interview materials for 2020

Public account: Kylin bug