Implementation of distributed locking on Redis to improve performance

  • 2020-05-12 06:26:10
  • OfStack

Background:

In many Internet product applications, some scenarios need to be locked, such as: seckill, global increment ID, floor generation and so on. Most of the solutions are based on the implementation of DB, Redis is a single-process single-thread mode, adopts the queue mode to turn concurrent access into serial access, and there is no competition between multiple clients for the connection of Redis.

The project practice

There are many cases in which distributed locks are used in the task queue. When the operations that can be processed asynchronously in the business logic are put into the queue and processed in other threads, the distributed locks are used in the queue to ensure the 1-uniqueness of enqueue and enqueue. As for the logical analysis of the redis queue, I will summarize it in the next time and skip it here.


Next, the logic code of the distributed lock implemented by redis is analyzed and understood in detail:

1. In case the lock cannot be released due to special reasons, the lock will be granted a survival time (through the parameter setting of lock method or the default value) after the successful addition of the lock, and the lock beyond the survival time will be automatically released.

2. The life time of the lock is short by default (second level, see lock method for details), so if a lock needs to be locked for a long time, the life time of the lock can be extended by expire method for an appropriate time, such as calling expire in a loop
3. System level lock when the process crash occurs for any reason, the operating system will recycle the lock itself, so there will be no resource loss.
4. But distributed locks are different. If the primary setting takes a long time and unlock is not called due to various reasons such as process crash or other exceptions, the lock becomes a garbage lock for the rest of the time, resulting in other processes or processes restarting and not being able to enter the locked area.


<?php
 
require_once 'RedisFactory.php';
 
/**
*  in  Redis  A distributed lock implemented on 
*/
class RedisLock {
  
// The singleton pattern 
  private static $_instance = null;
  public static function instance() {
    if(self::$_instance == null) {
      self::$_instance = new RedisLock();
    }
    return self::$_instance;
  }
 
  
//redis Object variables 
  private $redis;
  
// An array of locked flag names 
  private $lockedNames = array();
 
  public function __construct() {
    
// To obtain 1 a  RedisString  The instance 
    $this->redis = RedisFactory::instance()->getString();
  }
 
  
/** 
  
*  lock 
  
*
  
* @param string  The identifying name of the lock 
  
* @param int  The wait timeout for failure to acquire a lock ( seconds ),  In that time 1 Keep trying to get the lock until the timeout .  for  0  Returns directly after a failure without waiting 
  
* @param int  The maximum lifetime of the current lock ( seconds ),  Must be greater than  0 .  If the lock is not released after the survival time ,  The system will automatically force it to release 
  
* @param int  The time interval between hanging and trying again after failure to acquire the lock ( microseconds )
  
*/
  public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
    if(empty($name)) return false;
 
    $timeout = (int)$timeout;
    $expire = max((int)$expire, 5);
    $now = microtime(true);
    $timeoutAt = $now + $timeout;
    $expireAt = $now + $expire;
 
    $redisKey = "Lock:$name";
    while(true) {
      $result = $this->redis->setnx($redisKey, (string)$expireAt);
      if($result !== false) {
        
// right $redisKey Set survival time 
        $this->redis->expire($redisKey, $expire);
        
// Record the maximum survival time in 1 In an array of 
        $this->lockedNames[$name] = $expireAt;
        return true;
      }
 
      
// In seconds, return $redisKey  The remaining survival time 
      $ttl = $this->redis->ttl($redisKey);
      
// TTL  Less than  0  said  key  Live time is not set on (key  It doesn't exist ,  Because the front  setnx  It will automatically create )
      
//  If that happens ,  That's the process in some instance  setnx  After a successful  crash  Cause to follow  expire  Not called .  You can set it directly  expire  And use it for your own use 
      if($ttl < 0) {
        $this->redis->set($redisKey, (string)$expireAt, $expire);
        $this->lockedNames[$name] = $expireAt;
        return true;
      }
 
      
//  No wait or timeout is set 
      if($timeout <= 0 || microtime(true) > $timeoutAt) break;
 
      
//  hang 1 Try again sometime 
      usleep($waitIntervalUs);
    }
 
    return false;
  }
 
  
/**
  
*  Adds a specified lifetime to the current lock ( seconds ),  Must be greater than  0
  
*
  
* @param string  The identifying name of the lock 
  
* @param int  Time to live ( seconds ),  Must be greater than  0
  
*/
  public function expire($name, $expire) {
    if($this->isLocking($name)) {
      if($this->redis->expire("Lock:$name", max($expire, 1))) {
        return true;
      }
    }
    return false;
  }
 
  
/**
  
*  Determines whether you currently have a lock with the specified name 
  
*
  
* @param mixed $name
  
*/
  public function isLocking($name) {
    if(isset($this->lockedNames[$name])) {
      return (string)$this->lockedNames[$name] == (string)$this->redis->get("Lock:$name");
    }
    return false;
  }
 
  
/**
  
*  Release the lock 
  
*
  
* @param string  The identifying name of the lock 
  
*/
  public function unlock($name) {
    if($this->isLocking($name)) {
      if($this->redis->deleteKey("Lock:$name")) {
        unset($this->lockedNames[$name]);
        return true;
      }
    }
    return false;
  }
 
  
/**  Release all locks currently acquired  */
  public function unlockAll() {
    $allSuccess = true;
    foreach($this->lockedNames as $name => $item) {
      if(false === $this->unlock($name)) {
        $allSuccess = false;
      }
    }
    return $allSuccess;
  }
}

Much of this code is annotated, and with a good understanding, it's easy to see how to implement distributed locking in redis.


Related articles: