The interviewer asked redis how to design distributed locks to satisfy him?

Time:2021-7-30

preface

I have also checked a lot of information about distributed locks. I feel that many methods are not perfect, or I don’t know why, so I sorted out this article. I hope it is useful to you. If there is something wrong, please leave a message for correction.

Let’s talk about what the problem is first? Look directly at the code

$stock = $this->getStockFromDb();// Query remaining inventory
 if ($stock>0){
       $this->ReduceStockInDb(); //  Perform inventory reduction in the database
       echo "successful";
 }else{
    Echo "insufficient inventory";
 }

In a very simple scenario, when the user places an order, we query whether the commodity inventory is enough. If not, we directly return the error message of insufficient inventory. If the inventory is enough, we directly store – 1 in the database, and then return success. In business logic, there is no problem with this code.

However, there are serious problems with this code.

If the inventory is only 1 and the concurrency is high, for example, two requests execute this code at the same time, find that the inventory is 1, and then go to the database to execute stock-1 smoothly. In this way, the inventory will become – 1, and then oversold will occur. What I just said is that two requests are executed at the same time. If thousands of requests call at the same time, It can be seen that the loss caused is very large. So, some smart people came up with a solution as follows.

Everyone knows that redis has a setnx command. It doesn’t matter if you don’t know. I’ve checked it for you
The interviewer asked redis how to design distributed locks to satisfy him?

Let’s optimize the above code

version-1

$lock_key="lock_key";
 $res = $redis->setNx($lock_key, 1);
 if (!$res){
       return "error_code";
 }

 $stock = $this->getStockFromDb();// Query remaining inventory
 if ($stock>0){
       $this->ReduceStockInDb(); //  Perform inventory reduction in the database
       echo "successful";
 }else{
    Echo "insufficient inventory";
 }

$redis->delete($lock_key);
  • The first request will go to setnx. Of course, the result will return true because of lock_ If the key does not exist, the following business logic will proceed normally. After the task is executed, lock it_ Delete the key so that the next request comes in and repeats the above logic
  • The second request will also execute setnx, and the result returns false because of lock_ The key already exists, and then directly returns the error message (this is why the system is busy when you rush to buy seckill products on double 11), and does not execute the operation of inventory minus 1
  • Some students may have doubts. Don’t we say in the case of high concurrency? If two requests set NX at the same time, the obtained results are not all true, and the business logic will be executed at the same time. Isn’t the same problem unresolved? However, you should understand that redis is single threaded and atomic. Different request execution setnx is executed sequentially, so you don’t have to worry about this.

It seems that the problem has been solved, but it is not.

The pseudo code here is simple, just query the inventory, and then subtract 1. However, the situation in the real production environment is very complex. In some extreme cases, the program is likely to report an error and crash. If the program reports an error after locking for the first time, the lock will always exist, and the next request will never come in, So let’s continue to optimize

version-2

Try {// a new try catch process is added, so that the program will delete the lock in case of an error
   $lock_key="lock_key";
   $expire_ time = 5;// Add the expiration time of the new lock, so that the lock will not be occupied all the time
   $res = $redis->setNx($lock_key, 1, $expire_time);
   if (!$res){
         return "error_code";
   }

   $stock = $this->getStockFromDb();// Query remaining inventory
   if ($stock>0){
         $this->ReduceStockInDb(); //  Perform inventory reduction in the database
         echo "successful";
   }else{
      Echo "insufficient inventory";
   }
 }finally {
    $redis->delete($lock_key);
}
  • Add the expiration time to setnx, so that at least the lock will not exist forever and become a deadlock
  • Do try catch processing. If the program throws an exception and deletes the lock, it is also to solve the deadlock problem

This time, the deadlock problem has been solved, but the problem still exists. You can think about what problems still exist, and then look down.

The existing problems are as follows

  • Our expiration time is 5 seconds. What if the request takes 6 seconds to execute? What’s the difference between exceeding that second and not locking it? In fact, there is not only that, but also a more serious problem. For example, the second request is also executed for 6 seconds. When the second request comes in after 1 second, the lock added by the second request will be deleted. If the concurrency is large all the time, the lock is no different from not adding.
  • The most direct way to solve the above problems is to lengthen the expiration time, but this is not the final way to solve the problem. Setting the time too long will also lead to new problems. For example, the machine crashes for various reasons and needs to be restarted. Then you set the lock for 1 year without deleting it. Can you wait another year after the machine is restarted? In addition, the solution of setting a fixed value in this way is not allowed in computers. The once “Millennium Bug” problem is caused by similar reasons
  • When adding the timeout, be sure to add it at one time to ensure its atomicity. Do not set expire after setnx_ Time. In this case, if the system hangs at the moment after setnx, the lock will still become a permanent deadlock
  • In fact, the main reason for the above problem is that request 1 will delete the lock of request 2, so the lock needs to be unique.

Let’s continue to optimize

version-3

Try {// a new try catch process is added, so that the program will delete the lock in case of an error
   $lock_key="lock_key";
   $expire_ time = 5;// Add the expiration time of the new lock, so that the lock will not be occupied all the time
   
   $client_ id = session_ create_ id();  // Generate a unique ID for each request
   $res = $redis->setNx($lock_key, $client_id, $expire_time);
   if (!$res){
         return "error_code";
   }

   $stock = $this->getStockFromDb();// Query remaining inventory
   if ($stock>0){
         $this->ReduceStockInDb(); //  Perform inventory reduction in the database
         echo "successful";
   }else{
      Echo "insufficient inventory";
   }
 }finally {
   if ($redis->get($lock_ key) == $client_ ID) {// add a judgment here to ensure that the lock deleted each time is the lock added in the current request, so as to avoid deleting the lock added in other requests by mistake
      $redis->delete($lock_key);
   }
   
}
  • We generated a unique client for each request_ ID, and the value is written to lock_ Key
  • When you delete a lock at last, you will judge the lock first_ Whether the key is generated by the request. If not, it will not be deleted

However, there are still problems with the above scheme. In the end, redis judges the get operation first and then deletes it. It is a two-step operation, which does not guarantee its atomicity. The multi-step operation of redis can use Lua script to ensure its atomicity. In fact, it doesn’t need to feel too strange to see Lua. It’s just a language, The function here is to package multiple redis operations into one command for execution to ensure atomicity

version-4

Try {// a new try catch process is added, so that the program will delete the lock in case of an error
   $lock_key="lock_key";
   $expire_ time = 5;// Add the expiration time of the new lock, so that the lock will not be occupied all the time
   
   $client_ id = session_ create_ id();  // Generate a unique ID for each request
   $res = $redis->setNx($lock_key, $client_id, $expire_time);
   if (!$res){
         return "error_code";
   }

   $stock = $this->getStockFromDb();// Query remaining inventory
   if ($stock>0){
         $this->ReduceStockInDb(); //  Perform inventory reduction in the database
         echo "successful";
   }else{
      Echo "insufficient inventory";
   }
 }finally {
    $script = '// the Lua script is used here to compare the atomicity of the two-step delete operation after get
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$lock_key, $client_id], 1);
   
}

After such encapsulation, the distributed lock should be more perfect. Of course, we can further optimize the user experience

  • Now, for example, after a request comes in, if the request is locked, it will be returned to the user immediately. If the request fails, please try again. We can appropriately extend this time and do not immediately return it to the user. The experience will be better
  • The specific method is that the user requests to come in. If a lock is encountered, you can wait for some time and try again. If the lock is released during the retry, the request can succeed

version-5

$retry_ times = 3; // retry count
$usleep_ times = 5000;// Retry interval

  Try {// a new try catch process is added, so that the program will delete the lock in case of an error
   
   
    $lock_key="lock_key";
    $expire_ time = 5;// Add the expiration time of the new lock, so that the lock will not be occupied all the time
    while($retry_times > 0){
      $client_ id = session_ create_ id();  // Generate a unique ID for each request
      $res = $redis->setNx($lock_key, $client_id, $expire_time);
      if ($res){
           break;
      }
      Echo "attempt to acquire lock again";
      $retry_times--;
      usleep($usleep_times);
   }
   if (!$ Res) {// if the lock is not obtained after three retries, an error message is returned to the user
         return "error_code";
   }
   $stock = $this->getStockFromDb();// Query remaining inventory
   if ($stock>0){
         $this->ReduceStockInDb(); //  Perform inventory reduction in the database
         echo "successful";
   }else{
      Echo "insufficient inventory";
   }
 }finally {
    $script = '// the Lua script is used here to compare the atomicity of the two-step delete operation after get
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$lock_key, $client_id], 1);
   
}

Of course, the above distributed locks are not perfect. For example, redis master-slave synchronization delay will cause problems. For example, the idea of redismission implementation in Java is very good. If you are interested, you can look at the source code. That’s all for today. If you are interested, you can leave a message for discussion

Recommended Today

Notes on basic learning of ruby metaprogramming

Note 1:The code contains variables, classes and methods, which are collectively referred to as language construct. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # test.rb class Greeting  def initialize(text)   @text = text  end    def welcome   @text  end end my_obj = Greeting.new(“hello”) […]