Objective
There are three types of distributed locks:
1. Database optimistic lock;
2. Redis-based distributed locks;
3. Zookeeper-based distributed locks .
this blog will introduce the second way to implement distributed locks based on Redis. Although there are a variety of blogs on the web that introduce the implementation of Redis distributed locks, their implementations have a variety of problems, and in order to avoid fraught, this blog will detail how to implement Redis distributed locks correctly.
Reliability
First, to ensure that a distributed lock is available, we must at least ensure that the lock implementation meets the following four conditions:
- Mutex. At any given moment, only one client can hold the lock.
- Deadlocks do not occur. Even if a client crashes while holding a lock and does not actively unlock it, it can ensure that subsequent other clients can lock.
- is fault tolerant. As long as most REDIS nodes are functioning properly, the client can lock and unlock.
- The bell must also be fastened to the person. Lock and unlock must be the same client, the client itself can not be added to the lock to the solution.
Code implementation Component Dependencies
First we will introduce the Jedis open source component through Maven and add the following code to the Pom.xml file:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
Add lock Code
Correct posture
Talk is cheap, show me the code. Show the code first, and then take it slowly to explain why this is achieved:
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";
/**
* Try to get a distributed lock
* @param jedis Redis client
* @param lockKey lock
* @param requestId request ID
* @param expireTime overtime
* @return whether to succeed
*/
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;
}
}
As you can see, we lock a line of code: Jedis.set (String key, String value, String nxxx, string expx, int time), the set () method has five parameters:
- The first one is key, we use key to lock, because key is unique.
- The second is value, we pass the RequestID, many children's shoes may not understand, there is a key as a lock is enough, why use value? The reason is that when we talk about reliability, distributed locks to meet the fourth condition of the bell must also ring people, by assigning value to RequestID, we know that this lock is the request added, when the unlock can have a basis. RequestID can be generated using the Uuid.randomuuid (). ToString () method.
- The third is nxxx, which we filled out with NX, meaning set if not EXIST, that is, when key does not exist, we do the set operation, if the key already exists, then do nothing;
- The fourth is EXPX, this parameter we pass is px, meaning we want to give this key an expiration setting, the time is determined by the fifth parameter.
- The fifth is time, echoing the fourth parameter, which represents the expiration of key.
In general, executing the Set () method above will result in only two results: 1. There is currently no lock (key does not exist), then the lock operation is performed, and the lock is set to an expiration date, and value represents the locked client. 2. There is a lock existing, do not do any operation.
Cautious's children's shoes will be discovered, and our lock code satisfies the three conditions described in our reliability. First, set () adds the NX parameter, which guarantees that if a key exists, the function does not invoke success, that is, only one client can hold the lock and satisfy the mutex. Second, because we set an expiration time on the lock, even if the lock holder fails to unlock after the subsequent crash, the lock is automatically unlocked because it expires (that is, key is deleted) and no deadlock occurs. Finally, because we assign value to RequestID, which represents the client request identity for locking, the client can verify whether it is the same client when it is unlocked. Because we only consider the scenario of Redis standalone deployment, we do not consider fault tolerance.
Error Example 1
A more common example of errors is the use of jedis.setnx () and Jedis.expire () combination to implement the lock, the code is as follows:
publicstaticvoidwrongGetLock1(Jedis jedis, String lockKey, String requestId,intexpireTime) { Long result = jedis.setnx(lockKey, requestId); if(result ==1) { // If the program suddenly crashes here, the expiration time cannot be set, and a deadlock will occur jedis.expire(lockKey, expireTime); }}
Public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
If (result == 1) {
// If the program suddenly crashes here, the expiration time cannot be set and a deadlock will occur.
Jedis.expire(lockKey, expireTime);
}
}
The action of the Setnx () method is that the set IF not Exist,expire () method is to add an expiration time to the lock. At first glance it seems as if the previous set () method results, however, since this is a two Redis command and does not have atomicity, if the program suddenly crashes after executing setnx (), the lock does not set an expiration time. Then a deadlock will occur. The reason people do this online is because the low version of Jedis does not support the set () method of multiple parameters.
Error Example 2
This kind of error example is more difficult to find, and the implementation is more complex. Implementation idea: Use the jedis.setnx () command to implement the lock, where key is the lock, value is the lock expiration time. Execution process: 1. The lock is attempted by the Setnx () method, and if the current lock does not exist, the lock is returned successfully. 2. If the lock already exists, gets the expiration time of the lock, compared with the current time, if the lock has expired, set a new expiration time, return lock success. 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 the current lock does not exist, return the lock successfully.
If (jedis.setnx(lockKey, expiresStr) == 1) {
Return true;
}
// If the latch is in, get the lock expiration time
String currentValueStr = jedis.get(lockKey);
If (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// The lock has expired, get the expiration time of the previous lock, and set the expiration time of the current lock.
String oldValueStr = jedis.getSet(lockKey, expiresStr);
If (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
/ / Consider the case of multi-threaded concurrency, only one thread has the same set value as the current value, it has the right to lock
Return true;
}
}
// In other cases, always return lock failure
Return false;
}
So where is this code problem? 1. Because the client itself generates an expiration time, it needs to be forced to require that the time for each client in the distribution must be synchronized. 2. When the lock expires, if more than one client executes the Jedis.getset () method at the same time, the expiration time of the lock on the client may be overwritten by other clients, although only one client can eventually be locked. 3. The lock does not have the owner's identity, which means that any client can unlock it.
Unlock Code
Correct posture
Let's show the code first, and then explain why this is done:
Public class RedisTool {
Private static final Long RELEASE_SUCCESS = 1L;
/**
* Release distributed locks
* @param jedis Redis client
* @param lockKey lock
* @param requestId request ID
* @return whether the release is 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;
}
}
As you can see, we only need two lines of code to unlock it! The first line of code, we wrote a simple Lua script code, the last time to see this programming language or in the " Hacker and painter", did not expect this time to use. The second line of code, we pass the LUA code to the Jedis.eval () method, and the parameter keys[1] assignment to lockkey,argv[1] is assigned to RequestID. The eval () method is to give the LUA code to the Redis server for execution.
So what is the function of this LUA code? In fact, it is very simple to first get the value of the lock, check if it is equal to RequestID, and delete the lock (unlock) if it is equal. So why use the Lua language to implement it? Because you want to make sure that the above operation is atomic. For questions about non-atomicity, you can read "Unlocking code-error Example 2". So why does the eval () method ensure atomicity, derived from the features of Redis, and the following is part of the website's interpretation of the eval command:
Simply put, the LUA code is executed as a command when the eval command executes the LUA code, and Redis executes other commands until the eval command finishes.
Error Example 1
The most common unlocking code is to delete a lock directly using the Jedis.del () method, a way to unlock it without first judging the owner of the lock, which can cause any client to be unlocked at any time, even if the lock is not.
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
jedis.del(lockKey);
}
Error Example 2
This unlock code at first glance is also no problem, even I have almost achieved this, and the correct posture is similar, the only difference is divided into two commands to execute, the code is as follows:
Public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
/ / Determine whether the lock and unlock are the same client
If (requestId.equals(jedis.get(lockKey))) {
// If at this time, the lock is suddenly not the client, it will be unlocked by mistake.
Jedis.del(lockKey);
}
}
As a code comment, the problem is that if the Jedis.del () method is called, the lock will be unlocked when it is not part of the current client. So is there really such a scenario? The answer is yes, for example, client a plus lock, after a period of time after client a unlocked, before executing Jedis.del (), the lock suddenly expired, at this time, Client B attempted to lock successfully, then client A and then execute the Del () method, then the lock of Client B is released.
Summarize
This article mainly introduces how to use Java code to realize the Redis distributed lock correctly, and gives two more classic error examples for lock and unlock. In fact, it is not difficult to achieve a distributed lock through Redis, as long as the four conditions in the reliability are guaranteed to be met. Although the internet has brought us convenience, as long as there are problems can Google, but the online answer must be right? In fact, so we should always keep the spirit of questioning, many want more verification.
If Redis is a multi-machine deployment in your project, you can try to implement distributed locks using Redisson, which is the official Redis Java component, which is provided in the Reference reading section.
The correct implementation of Redis distributed Locks (Java edition)