As we all know, redis distributed lock can be implemented using the SET command, but only using this command? Whether CAP theory should also be considered.
If only it were as simple as this, the distributed locking scheme we use in our development might be, depending on the complexity of the business and the amount of concurrency.
Let’s talk about how to use distributed locks correctly in high-concurrency scenarios.
Before we get into distributed locks, let’s look at some of the issues that will be covered:
What scenarios use distributed locks?
Only one thread can read or write a [shared resource] at a time. That is, in high concurrency scenarios, only one thread should be allowed access at a time to ensure data correctness.
This is where distributed locking is needed.
In short, distributed locks are used to control that only one thread can access a protected resource at a time.
Distributed locking features
-
Deadlock-free: all have the opportunity to acquire the lock even if the client that acquired the lock dies.
-
Mutually exclusive: Only one thread can hold the lock at any one time;
You can use the SETNX key value command to implement the mutual exclusion feature.
If the key does not exist, set value to the key, otherwise do nothing.
The return value of this command can be either of the following:
- 1: Indicates that the configuration is successful.
- 0: the key is not set successfully.
Examples are as follows:
Success:
> SETNX lock:101 1 (integer) 1
Failure:
> SETNX lock:101 2 (integer) 0
As you can see, the successful ones are ready to start using shared resources.
Release the lock in a timely manner to give future applications a chance to obtain resources.
So how do you release the lock?
To release the lock, use the DEL command to delete the key. As follows:
> DEL lock:101 (integer) 1
The simple usage scheme of distributed lock is as follows:
Doesn’t that seem easy? What could be wrong with that? Read on for my analysis.
Firstly, there is a problem that the lock cannot be released in this scheme. The scenario is as follows:
- The service was restarted and the lock could not be released properly.
- The service logic is abnormal and cannot be executed
DEL
The instructions.
As a result, the lock will always be occupied, and other clients will not be able to get the lock.
Key Sets the expiration time
Better yet: Consider setting an expiration date on the key when the lock is acquired.
The following is an example:
> SETNX lock:101 1 // Obtain lock (integer) 1
> EXPIRE lock:101 60 // 60s EXPIRE delete (integer) 1
It can be seen that the lock will be released after 60 seconds, and other customers can apply for use.
To be honest, this is too low, and there are still problems.
As can be seen from the above example, locking and setting expiration time are two operation commands and are not atomic operations.
Imagine a situation where:
For example, if the first command is executed successfully, an exception occurs before the second command is executed, leading to the failure to set the “expiration time”, and thus the lock cannot be released.
Don’t panic, after Redis 2.6.X, the SET command parameters are extended, that is, SET the value when the key does not exist, and SET the expiration time, which can solve the problem mentioned above, that is, satisfy the atomicity.
SET keyName value NX PX 30000
- NX: indicates when
keyName
When they don’t existSET
Success, so that the lock can be successfully acquired; - PX 30000: Indicates that the acquired lock has an expiration time of 30 seconds.
There seems to be nothing wrong with it. No, on closer inspection, it’s still not rigorous enough. Think about it. It’s possible to release a lock you didn’t put on yourself.
Think of…
The lock released is not its own
Name a situation where the lock released is not your own:
- A The lock is obtained successfully and the expiration time is set to 30 seconds.
- A is slow to execute for some reason (network delay may be A problem, GC is in progress, etc.). After 30 seconds, the execution has not been completed, but the lock has expired and been released.
- B comes to apply for locking successfully.
- And then A starts executing
DEL
Release the lock command, which releases the lock on B.
So, there is a key point to note: you can only release locks that you have requested.
In short, the bell must be tied
Resolve the issue of releasing locks that are not your own:
You can set a unique identifier as a value at lock time. When releasing the lock, check whether the value obtained is the same as the value of the lock. If the value is the same, the lock cannot be released.
The pseudocode is as follows:
// Determine the unique identifier between value and lock
if (redis.get("lock:101").equals(value)){
redis.del("lock:101"); }}Copy the code
It’s also a matter of atomicity, because it’s also a combination of the GET + DEL instructions.
At this point, we can consider Lua scripts, so that the process of judging and deleting is atomic.
// Get the lock value and compare it with ARGV[1]
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Copy the code
Using the script above, each lock is assigned a random string “signature” that will only be removed if the client “signature” of the lock matches the value of the lock.
IO /topics/dist…
So far, the above modified (optimized) solution is relatively complete, and it is the most used solution in the industry.
What is the appropriate expiration time for a lock?
Of course, the lock expiration time should not be arbitrarily written, it is usually selected based on the results of multiple tests, such as after multiple rounds of pressure test, the average execution time is 300 ms.
Then our lock expiration time should be scaled up to 3-4 times the average execution time.
Some friends may ask: why magnify 3 or 4 times?
This is called saving for everything: considering the operation logic of the lock if there are network IO operations, the network will not always be stable, so you need to leave some time to buffer.
We can have the thread that acquired the lock start a daemon thread that can be used to “renew” a lock that is about to expire.
When locking, set an expiration time, and the client starts a “daemon thread” to periodically check the expiration time of the lock.
If the lock is about to expire, but the business logic has not been completed, the lock is automatically renewed and the expiration time is reset.
We all know the truth, but how to write such code.
Google will tell you that there is a library that encapsulates all of this work, and you can use it. It’s called Redisson.
When distributed locks are used, they are automatically renewed to avoid lock expiration. This daemon thread is also known as the “watchdog” thread.
After many times of optimization, the scheme is also relatively “perfect”, and the corresponding model is abstracted as follows.
- First of all by
SET keyName randomValue NX PX expireTime
And start the daemon thread to renew the client lock that is about to expire but has not finished executing. - Then the client executes the business logic and operates the shared resources.
- At last,
Lua
The script releases the lock, usually by getting the lock, determining whether the lock was added by itself, and then executing itDEL operation
.
The solution is OK, and the optimization points that come up with these have beaten a lot of programmers.
For extreme programmers, you might consider:
- Where do I add and unlock the code?
- How do you think about reentrant locking?
- Should you consider the problems posed by master-slave architecture?
- .
I won’t go into the discussion here. Those who are interested can discuss it in the comments section.
conclusion
- Here, distributed locks involved in the problem, and how to use the scheme are finished, I believe you see here, how much will be a little harvest. In fact, no matter what do distributed locks will exist problems, it is important to think about their own process.
- At this point, it is recommended that you close your eyes and review in your mind what distributed locking is doing at each step, why it is doing so, and what problem it is solving.
- For the design of the system, the starting point is different, the design of the scheme is not the same, there is no best framework, there is no best scheme, only the most appropriate.
Database and cache consistency issues, just read this article
Golang unlimited open Goroutine? How do I limit the number of Goroutines?
Golang GMP principle and scheduling process to you understand the whole ~