preface

In daily development, business scenarios such as placing orders in seconds and snatching red envelopes all need to use distributed locks. Redis is ideal for use as a distributed lock. This article will be divided into seven schemes to explore the correct use of Redis distributed lock. If there is an incorrect place, welcome to point out ha, study together and progress together.

Public number: a boy picking up snails

  • What is distributed locking

  • Option 1: SETNX + EXPIRE

  • Solution 2: SETNX + value Is (system time + expiration time).

  • Option 3: Use the Lua script (with SETNX + EXPIRE directives)

  • SET EX PX NX

  • SET EX PX NX + check the unique random value, then release the lock

  • Scheme 6: Open source framework :Redisson

  • Scheme 7: distributed Redlock for multi-machine implementation

  • Github address, thanks to each star

Github.com/whx123/Java…

What is distributed locking

Distributed lock is the realization of a lock that controls the access to shared resources by different processes in a distributed system. If a critical resource is shared between different systems or hosts on the same system, mutual exclusion is often required to prevent interference and ensure consistency.

Let’s take a look at the characteristics of a reliable distributed lock:

  • Mutual exclusion: Only one client can hold the lock at any time.
  • Lock timeout release: Hold lock timeout, which can be released to prevent unnecessary waste of resources and prevent deadlocks.
  • Reentrancy: once a thread has acquired a lock, it can request a lock again.
  • High performance and high availability: Locking and unlocking must have a low cost while ensuring high availability to avoid distributed lock failure.
  • Security: A lock can only be deleted by the holding client, not by other clients

Redis distributed lock scheme 1: SETNX + EXPIRE

The setnx+ expire command immediately comes to mind when it comes to Redis distributed locks. Use setnx to rob the lock. If the lock is captured, use expire to set an expiration time for the lock.

SETNX is short for SET IF NOT EXISTS. The routine command format is SETNX key value. SETNX returns 1 on success if the key does not exist, and 0 if the key already exists.

For example, if a commodity on an e-commerce website performs seckilling, key can be set to key_resource_id and value can be set to any value. The pseudocodes are as follows:

If (jedis.setnx(key_resource_id,lock_value) == 1) {// Add lock expire (key_resource_id, 100); Try {do something // service request}catch(){} finally {jedis.del(key_resource_id); // Release the lock}}Copy the code

In this scenario, however, the setnx and EXPIRE commands are separated and are not atomic operations. If the process crashes after executing the setnx lock or is about to restart maintenance before executing the expire set, the lock will be “immortal” and will never be acquired by another thread.

Redis distributed lock scheme 2: SETNX + value The value is (system time + expiration time).

In order to solve the first solution, if the abnormal lock cannot be released, some friends think that the expiration time can be included in the value of setnx. If the lock fails, check the value again. The locking code is as follows:

long expires = System.currentTimeMillis() + expireTime; // System time + Set expiration time String expiresStr = string.valueof (Expires); If (jedis.setnx(key_resource_id, expiresStr) == 1) {return true; } String currentValueStr = jedis. Get (key_resource_id); If (currentValueStr! = null && long.parselong (currentValueStr) < system.currentTimemillis ()) { String oldValueStr = jedis. GetSet (key_resource_id, expiresStr); oldValueStr = jedis. if (oldValueStr ! = null && oldValueStr. Equals (currentValueStr)) {return true; }} return false; }Copy the code

The advantage of this scheme is that it cleverly removes the operation of expire setting the expiration time separately and puts the expiration time in the value of setnx. The lock cannot be released when an exception occurs in solution one. But there are other drawbacks:

  • The expiration time is generated by the client (System.currentTimemillis () is the current System time). In a distributed environment, the time on each client must be synchronized.
  • If jedis.getSet() is executed by multiple concurrent clients when the lock expires, only one client can successfully add the lock. However, the expiration time of the client lock may be overwritten by other clients
  • The lock does not have the unique identity of the holder and may be released or unlocked by other clients.

Redis distributed lock solution 3: use Lua script (including SETNX + EXPIRE directives)

In fact, we can also use Lua scripts to ensure atomicity (including setnx and EXPIRE directives). Lua scripts are as follows:

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;
Copy the code

The locking code is as follows:

String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"; Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values)); Return result.equals(1L);Copy the code

This plan still has shortcoming, as to what shortcoming, you ponder first. Or you can think about it. Which is better than plan two?

Redis distributed lock solution solution 4: SET extended command (SET EX PX NX)

In addition to using the Lua script to ensure the atomicity of SETNX + EXPIRE, we can also use Redis SET to extend parameters. (the SET key value [EX seconds] [PX milliseconds] [NX | XX]), it is also a atomic!

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

  • NX: Indicates that the set succeeds only when the key does not exist. That is, only the first client can obtain the lock, and other clients can obtain the lock only after the lock is released.
  • EX seconds: specifies the key expiration time, in seconds.
  • PX milliseconds: Sets the expiration time of the key, in milliseconds
  • XX: Set this value only when the key exists

Pseudo code demo is as follows:

Set (key_resource_id, lock_value, "NX", "EX", 100s) == 1){try {do something}catch(){} finally {jedis.del(key_resource_id); // Release the lock}}Copy the code

Still, there could be problems with this plan:

  • Fault 1: The lock expires and the service is not finished. Let’s say thread A has successfully acquired the lock and has been executing the code for the critical section. But after 100s, it’s not done yet. However, at this point, the lock has expired, and thread B is requesting it again. Thread B obviously gets the lock and starts executing the code for the critical section. The problem is that critical sections of business code are not executed strictly serially.
  • Fault two: The lock is deleted by another thread. Let’s say thread A finishes and releases the lock. But it does not know that the current lock may be held by thread B (thread A may release the lock when the expiration time has expired and thread B may come in and take possession of the lock). Thread A has released thread B’s lock, but thread B’s critical business code may not be finished yet.

SET EX PX NX + check the unique random value, then delete

If the lock is deleted by another thread, set the value to a unique random number that marks the current thread. If the lock is deleted, check it. The pseudocode is as follows:

If (jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){// try {do something // catch(){} finally {// Check whether the current thread is holding the lock (uni_request_id.equals(jedis.get(key_resource_id))) { jedis.del(lockKey); // Release the lock}}}Copy the code

In this case, determining whether the current thread has added a lock and releasing a lock is not an atomic operation. If jedis.del() is called to release the lock, the lock may no longer belong to the current client.

For more rigor, lua scripts are often used instead. The lua script is as follows:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;
Copy the code

Redis distributed locking scheme 6: Redisson framework

In scheme 5, the lock may expire and the service may not be completed. Some people think that setting the lock expiration time slightly longer is ok. In fact, we think about whether we can give the thread that obtains the lock, open a regular daemon thread, every once in a while to check whether the lock still exists, exists to extend the expiration time of the lock, prevent the lock from being released in advance.

The current open source framework, Redisson, addresses this problem. Let’s take a look at Redisson’s underlying schematics:

As soon as the thread successfully adds the lock, a watch Dog will be started. It is a background thread that will check every 10 seconds. If thread 1 still holds the lock, it will continuously extend the lifetime of the lock key. Therefore, Redisson uses Redisson to solve the problem that the lock expires and the service is not finished.

Redis distributed lock scheme 7: distributed lock Redlock+Redisson implemented by multiple machines

The first six solutions are only based on the standalone version of the discussion, not perfect. Redis is typically clustered:

If thread 1 acquires the lock on the master node of Redis, but the lock key has not been synchronized to the slave node. When the master node fails, a slave node becomes the master node. Thread two can acquire the lock with the same key, but thread one has already acquired the lock, so the lock security is lost.

To solve this problem, Antirez, the author of Redis, proposed an advanced distributed lock algorithm: Redlock. The core idea of Redlock is this:

Have multiple Redis Master deployments so they don’t all go down at the same time. These master nodes are completely independent of each other and there is no data synchronization between them. At the same time, you need to make sure that locks are acquired and released on multiple Master instances in the same way that they are acquired and released on single instances of Redis.

We assume that there are currently five Redis master nodes running these Redis instances on five servers.

The RedLock implementation steps are as follows

  • 1. Obtain the current time, in milliseconds.
  • 2. Lock the five master nodes in sequence. The client sets network connection and response timeouts that are less than the lock expiration time. (Assuming automatic lock failure time is 10 seconds, the timeout time is usually between 5-50 ms, let’s assume the timeout time is 50ms). If you time out, skip the master node and try the next master node as soon as possible.
  • 3. The client obtains the lock acquisition time by subtracting the start time (the time recorded in Step 1) from the current time. The lock is successful if and only if more than half of the Redis master nodes (N/2+1, in this case 5/2+1=3 nodes) are locked and used for less than the lock expiration time. (10s> 30ms+40ms+50ms+4m0s+50ms)
  • If a lock is obtained, the true duration of the key is changed by subtracting the time taken to obtain the lock.
  • If the lock fails to be acquired (the lock has not been acquired for at least N/2+1 master instances, or the lock has been acquired for longer than the valid time), the client must unlock all master nodes (even if some master nodes have not been locked at all, to prevent some from escaping).

The simplified steps are:

  • Lock requests are made to the five master nodes in sequence
  • Determine whether to skip the master node based on the timeout period.
  • If more than three nodes are locked successfully and the lock duration is shorter than the lock validity period, the lock is successfully locked.
  • If you fail to acquire the lock, unlock it!

Redisson implements the redLock version of the lock, you can go to see it

The public,

  • Welcome to the public number: a little boy picking up snails

Reference and thanks

  • Redis series: distributed locks
  • Brief analysis of Redis distributed lock solution
  • Redis distributed lock 🔒
  • Redlock: Redis distributed lock the most awesome implementation