The focus of this article is not to provide a working example of Redis distributed lock, but to understand the details of Redis distributed lock implementation and why it is done with graphics.

implementation

In the form of pseudo-code, a simple introduction to the implementation

Acquiring a lock

SET resource_name my_random_value NX EX 30
Copy the code
  • My_random_value: a random value that cannot be the same for competitors of the same key
  • NX: Sets the key only when the key does not exist
  • EX: Sets the expiration seconds

Release the lock

Compare and delete atoms by Lua script

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

Pseudo code

let randStr = Math.random();
let success = redis.set(key, randStr, 'EX'.30.'NX');
if(success){
    // Get the lock

    doSomething(); // Perform tasks to be done
    
    // Release the lock
    redis.call(` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`,key,randStr);
}else{
    // No lock was obtained
}

Copy the code

This makes Redis easy to use.

But why?

  • What does setting a random value do? Why is it necessary to compare random values before deleting?
  • Why does compare & Delete require lua scripts to implement atomic operations?
  • Is a single Redis node implementation really safe enough?

A few questions

What does setting a random value do? Why is it necessary to compare random values before deleting?

To answer the last question, why do we need to determine the value equality before releasing the lock? Why not just say del key?

First of all, one of the most basic functions of distributed lock is to achieve mutual exclusion. At a time, only one client should be able to hold the lock.

If you del key directly, the following situation may occur

  1. Client A acquires the lock and performs the task (Redis client, actually A server node)
  2. A is blocked and stuck
  3. The lock timeout is automatically released, but A continues to execute
  4. Client B acquires the lock and begins to perform its own task
  5. “A” wakes up, gets the job done, and saysdel keyRelease the lock that B is currently holding (but B is unaware that he is holding the lock and continues to perform his task)
  6. When another client C obtains the lock, both clients B and C hold it at the same time, which does not satisfy the mutual exclusion

(Activating the lifeline indicates that a client holds the lock, corresponding to the color)

Let’s see how we can avoid this problem by using random values.

First, when the client obtains the lock, the value set is a random string generated by each client.

In Step 5, because the random values of client A and client B are different, the else branch of lua script is only followed without deletion, so client C naturally has no opportunity to take advantage of it.

In general, random strings are used as “identifiers” for each client to ensure that the client can only delete its own locks and does not delete other clients by mistake.

Sequence diagrams using random strings

Looking at this, you might be thinking: No, clients B and C have overlapping parts (red and blue in Figure 1), indicating that they both hold locks. But AB also overlaps (blue and green in Figure 1). Is that a bug?

Sorry, that’s not a bug, it’s a feature.

The lock expiration time has to be added to prevent the lock from never being released due to a client crash.

Therefore, an appropriate lock expiration time can only be set based on the content of the task to be performed after the lock is acquired.

Alternatively, you can extend the expiration time before the expiration by using the client in some languages.


Why do lua scripts need to implement atomic operations for “compare & delete” lock release?

The above example uses a Lua script to release the lock

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

Why not just write it in code?

let value = redis.get(key);
if(randStr == value){
    redis.del(key);
}
Copy the code

Let’s cut to the drawing

As you can see from the figure, if you don’t use lua scripts to combine the compare & delete steps into an atomic operation, the original lock between the read and delete steps may expire and be acquired by another client, B.

However, the original client A still deletes the lock normally obtained by client B without knowing it. As A result, BC is held at the same time, which does not meet the mutual exclusion.


Is a single Redis node implementation really safe enough?

Distributed locking implemented by a single Redis node is generally sufficient, but keep in mind that it is not completely reliable.

The previous scenarios assume that the Redis service can run normally, but if a master node switch occurs, it may still result in multiple clients believing that they hold a lock.

This scenario is relatively simple:

  1. Client A obtains the lock
  2. The primary redis node is down, but the key for setting the lock has not been synchronized to the secondary node
  3. The slave node was selected as the new primary node, missing the key
  4. Client B obtains the lock, and client AB thinks that it owns the lock

Therefore, if you need to avoid the problem of data loss after a single Redis node crashes and switches, and achieve a higher level of protection, you can use multiple Redis nodes to achieve redlock

Note: Even RedLock is not 100% reliable, just more secure than Redis alone.


reference

Redis. IO/switchable viewer/dist…