We often encounter some problems such as repeated requests and concurrency in our work, and locking resources is a good means. Today, let's sort out several implementations of using redis as a distributed lock.
redis can be used for several commands: INCR, SETNX, and SET.
1. Lock with INCR
The idea of locking is that if the key does not exist, the value of the key will be initialized to 0 first, and then the INCR operation will be performed to add one. Then, other users add one hour when executing the INCR operation. If the number returned is greater than 1, it indicates that the lock is being used.
/*** 1, Client a requests the server to obtain the key. A value of 1 indicates that the lock has been obtained 2, Client B also requests the server to obtain the key. A value of 2 indicates that obtaining the lock failed 3, Client A executes the code and deletes the lock 4, After waiting for a period of time, client B obtains the key when requesting. The value of 1 indicates that the lock is obtained successfully 5, Client B executes the code and deletes the lock **/ $res = $redis->incr($key); // Self increment 1 $redis->expire($key, $ttl); // Set the validity period of the lock if($res == 1){ // Resource acquisition succeeded }else{ // The resource is occupied by another request }
2. Use SETNX to lock
The idea of locking is that if the key does not exist, set the key to value. If the key already exists, SETNX will not take any action.
/*** 1, Client A requests the server to set the value of key. If the setting is successful, it means that locking is successful 2, Client B also requests the server to set the value of key. If it fails, it means locking fails 3, Client A executes the code and deletes the lock 4, After waiting for a period of time, client B requests to set the value of key, and the setting is successful 5, Client B executes the code and deletes the lock **/ $res = $redis->setNX($key, $value); // When key Set when not present key=value $redis->expire($key, $ttl); // Set the validity period of the lock if($res){ // Resource acquisition succeeded }else{ // The resource is occupied by another request }
-
/
There is a problem with the above two methods. You will find that you need to set the key expiration time. So why set the key expiration time? If the request execution exits unexpectedly for some reason, causing the lock to be created but not deleted, the lock will always exist (redis does not set the expiration time of the key, which is permanent by default), so that it will always be in the locked state. So we need to add an expiration time to the lock in case of accidents.
But setting with Expire is not atomic. Therefore, you can also ensure atomicity through redis transactions. The above code should be optimized to:
// The first way is locking $redis->multi(); // Mark the beginning of a transaction block $res = $redis->incr($key); $redis->expire($key, $ttl); $redis->exec(); // Commit transaction if($res == 1){ // Resource acquisition succeeded }else{ // The resource is occupied by another request } // The second way is locking $redis->multi(); // Mark the beginning of a transaction block $res = $redis->setNX($key, $value); $redis->expire($key, $ttl); $redis->exec(); // Commit transaction if($res){ // Resource acquisition succeeded }else{ // The resource is occupied by another request }
Does the above code look cumbersome. Fortunately, since the official version 2.6.12 of redis, the SET command itself has included the function of setting the expiration time.
3. Lock with SET
/*** 1, Client A requests the server to set the value of key. If the setting is successful, it means that locking is successful 2, Client B also requests the server to set the value of key. If it fails, it means locking fails 3, Client A executes the code and deletes the lock 4, After waiting for a period of time, client B requests to set the value of key, and the setting is successful 5, Client B executes the code and deletes the lock **/ $res = $redis->set($key, $value, ['nx', 'ex' => $ttl]); //nx Representative when key Set when not present ex Set expiration time on behalf of if($res){ // Resource acquisition succeeded }else{ // The resource is occupied by another request }
4. Other issues
Although the above step has met our needs, we still need to consider other issues- 1. redis found that the lock failed. What should we do? Interrupt request or loop request- 2. In the case of circular requests, if one obtains the lock, is it easy to grab the lock when the other obtains the lock- 3. After the lock expires in advance, client A has not finished executing, and then client B obtains the lock. At this time, client A has finished executing. Will the lock of B be deleted when deleting the lock?
5. Solutions
- For question 1: use circular request to obtain lock
- For question 2: for the second question, when the loop requests to obtain the lock, add the sleep function and wait for a few milliseconds to execute the loop
- For question 3: the keys stored during locking are random. In this way, every time you delete a key, judge whether the value stored in the key is the same as that saved by yourself
do { //For question 1, use a loop $timeout = 10; $roomid = 10001; $key = 'room_lock'; $value = 'room_'.$roomid; //Assign a random value to question 3 $isLock = $redis->set($key, $value, 'ex', $timeout, 'nx');//ex second if ($isLock) { if ($redis->get($key) == $value) { //Prevent early expiration and delete locks created by other requests by mistake //Execute internal code $redos->del($key); continue;//Successfully deleted key And jump out of the loop } } else { usleep(5000); //Sleep, reduce lock grabbing frequency and relieve redis Pressure, for question 2 } } while(!$isLock);
That's all. Just like it
reference resources:
- http://ukagaka.github.io/php/2017/09/21/redisLock.html