Implementation of distributed lock and task queue _redis based on Redis

Source: Internet
Author: User
Tags current time php language redis usleep


First, the preface



Double 11 just before long, we all know in the days of cats, Jing Dong, suning and so on the website has many seconds to kill activities, such as at a certain moment snapped up a price 1999 now seconds to bargain as long as 999 of the mobile phone, will usher in the peak of a user request, there may be hundreds Guibaivan concurrent volume, to rob this phone, In high concurrency situations, the database server or file server application server is under great pressure, serious may be down, another problem is that the second kill is a quantity of things, such as a mobile phone only 10 of the amount of seconds to kill, then, in the case of high concurrency, Tens of thousands of data update databases (for example, 10 of the volume of the number of people will be robbed in the data set under some records minus 1), that time the order is very messy, it is easy to appear 10 units, the number of people to grab more than 10 of this serious problem. So, what can we do to solve the problems that we are talking about?



The techniques I share next can be used to address the above issues: distributed Locks and task queues .



Second, the realization of ideas



1.Redis realization of distributed lock idea



The idea is very simple, the main use of the Redis function is setnx (), this should be the implementation of distributed locks the most important function. The first is to save a task identification name (here with Lock:order as an example of an identifier) as a key to the Redis, and set an expiration date, if there is a Lock:order request, first through the SETNX () to see if the Lock:o Rder is inserted into the Redis, returns True if you can, and returns false. Of course, it's more complicated than that in my code, and I'll explain it further when I analyze the code.



2.Redis Implementation Task Queue



This implementation will use the above Redis distributed locking mechanism, mainly used in the Redis of the ordered collection of this data structure. For example, when the team team, through the Zset Add () function for the team, and out of the right, you can use the Zset getscore () function. You can also eject several tasks at the top.



This is the simple idea of implementing a distributed lock and task queue, and if you've seen a bit of ambiguity, see the next code implementation.



Third, Code Analysis



(a) first to analyze the Redis distributed lock code implementation



(1) In order to avoid the special cause of the lock can not be released, after the lock succeeds, the lock is given a lifetime (via the Lock method's parameter setting or using the default value), and the lifetime lock is automatically freed by default for a shorter life time (second level), so if it takes a long time to lock, The expire method can be used to extend the lifetime of the lock to the appropriate time, such as within the loop.



(2) System-level lock when the process for whatever reason occurs when the crash, the operating system will reclaim the lock itself, so there will be no resource loss, but the distributed lock does not need to be set for a long time, once due to a variety of reasons for the process crash or other anomalies caused unlock not be called The lock becomes a garbage lock for the remainder of the time, causing the other processes or processes to reboot and not enter the lock area.



First look at the lock implementation code: Here we need the main two parameters, one is $timeout, this is the wait time for the loop to acquire the lock, and in this time will always try to acquire the lock knowing that the timeout, if 0, means getting the lock to return directly after failure and no longer waiting; Another important parameter of the $expire, This parameter refers to the maximum lifetime, in seconds, of the current lock, which must be greater than 0, and will be automatically forced to release if the lock has not been released for more than a lifetime. The most important function of this parameter is to see the explanation in (1) above.



This gets the current time, and then the time to wait for the lock to fail (a timestamp), and then the maximum survival time for the lock. Here the Redis key is in this format: "Lock: The name of the lock, which begins in the loop, first inserting the data into the Redis, using the Setnx () function, which means inserting the data if the key does not exist, storing the maximum survival moment as a value, if the insert succeeds, The setting of the expiration time for the key. And put the key in the $lockedname array, return True, that is, lock success, if the key exists, it will not insert the operation, here is a rigorous operation, that is to get the remaining time of the current key, if this time is less than 0, Indicates that the key does not have a lifetime set (key does not exist because the previous setnx is created automatically) if this happens, it is an instance of the process Setnx successful crash causes the following expire not to be invoked, At this time can be directly set expire and lock for their own use. If the lock fails to wait or exceeds the maximum wait time, then the loop is exited, and the request continues after the $waitIntervalUs is separated.   This is an entire code analysis with locks.

/ **
   * Locked
   * @param [type] $ name the distinguished name of the lock
   * @param integer $ timeout Wait for the lock to time out in a loop. During this time, it will try to acquire the lock until it times out. If it is 0, it means to return directly without waiting.
   * @param integer $ expire The maximum lifetime (seconds) of the current lock, which must be greater than 0. If the lock has not been released beyond the lifetime, the system will automatically force the release
   * @param integer $ waitIntervalUs Interval (in microseconds) to suspend and retry after acquiring a lock failure
   * @return [type] [description]
   * /
  public function lock ($ name, $ timeout = 0, $ expire = 15, $ waitIntervalUs = 100000) {
    if ($ name == null) return false;

    // Get the current time
    $ now = time ();
    // Wait timeout when lock acquisition fails
    $ timeoutAt = $ now + $ timeout;
    // The maximum survival time of the lock
    $ expireAt = $ now + $ expire;

    $ redisKey = "Lock: {$ name}";
    while (true) {
      // Save the maximum survival time of rediskey in redis. After this time, the lock will be automatically released.
      $ result = $ this-> redisString-> setnx ($ redisKey, $ expireAt);

      if ($ result! = false) {
        // Set the key expiration time
        $ this-> redisString-> expire ($ redisKey, $ expireAt);
        // Place the lock flag in the lockedNames array
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      // Returns the remaining survival time of the given key in seconds
      $ ttl = $ this-> redisString-> ttl ($ redisKey);

      // ttl is less than 0, which means no time to live is set on the key (the key will not exist, because the previous setnx will be automatically created)
      // If this happens, it is that an instance of the process setnx crashes and the subsequent expire is not called
      // At this time, you can directly set expire and use the lock for your own use
      if ($ ttl <0) {
        $ this-> redisString-> set ($ redisKey, $ expireAt);
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      / ***** Circular request lock section ***** /
      // If the waiting time for lock failure is not set or the maximum waiting time has been exceeded, then exit
      if ($ timeout <= 0 || $ timeoutAt <microtime (true)) break;

      // Continue request after $ waitIntervalUs
      usleep ($ waitIntervalUs);

    }

    return false;
  }

Then look at the unlocked code analysis: unlocking is much simpler. The incoming parameter is the lock identifier. First, determine whether the lock exists. If it exists, you can delete the lock identifier from redis through the deleteKey () function.
/ **
   * Unlock
   * @param [type] $ name [description]
   * @return [type] [description]
   * /
  public function unlock ($ name) {
    // First determine if this lock exists
    if ($ this-> isLocking ($ name)) {
      // Delete the lock
      if ($ this-> redisString-> deleteKey ("Lock: $ name")) {
        // Clear the lock flag in lockedNames
        unset ($ this-> lockedNames [$ name]);
        return true;
      }
    }
    return false;
  }
方法 The method of deleting all locks on the paste is actually the same, and it only has a loop to traverse.
/ **
   * Release all currently acquired locks
   * @return [type] [description]
   * /
  public function unlockAll () {
    // This flag is used to indicate whether all locks were successfully released
    $ allSuccess = true;
    foreach ($ this-> lockedNames as $ name => $ expireAt) {
      if (false === $ this-> unlock ($ name)) {
        $ allSuccess = false;
      }
    }
    return $ allSuccess;
  }
The above is the summary and sharing of the entire set of ideas and code implementation of distributed locks with Redis. Here I attach the code of an implementation class. I have basically commented each line in the code for everyone to quickly understand and Can simulate applications. If you want to learn more, please look at the code of the entire class:
/ **
 * Implement distributed locks on redis
 * /
class RedisLock {
  private $ redisString;
  private $ lockedNames = [];

  public function __construct ($ param = NULL) {
    $ this-> redisString = RedisFactory :: get ($ param)-> string;
  }

  / **
   * Locked
   * @param [type] $ name the distinguished name of the lock
   * @param integer $ timeout Wait for the lock to time out in a loop. During this time, it will try to acquire the lock until it times out. If it is 0, it means to return directly without waiting.
   * @param integer $ expire The maximum lifetime (seconds) of the current lock, which must be greater than 0. If the lock has not been released beyond the lifetime, the system will automatically force the release
   * @param integer $ waitIntervalUs Interval (in microseconds) to suspend and retry after acquiring a lock failure
   * @return [type] [description]
   * /
  public function lock ($ name, $ timeout = 0, $ expire = 15, $ waitIntervalUs = 100000) {
    if ($ name == null) return false;

    // Get the current time
    $ now = time ();
    // Wait timeout when lock acquisition fails
    $ timeoutAt = $ now + $ timeout;
    // The maximum survival time of the lock
    $ expireAt = $ now + $ expire;

    $ redisKey = "Lock: {$ name}";
    while (true) {
      // Save the maximum survival time of rediskey in redis. After this time, the lock will be automatically released.
      $ result = $ this-> redisString-> setnx ($ redisKey, $ expireAt);

      if ($ result! = false) {
        // Set the key expiration time
        $ this-> redisString-> expire ($ redisKey, $ expireAt);
        // Place the lock flag in the lockedNames array
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      // Returns the remaining survival time of the given key in seconds
      $ ttl = $ this-> redisString-> ttl ($ redisKey);

      // ttl is less than 0, which means no time to live is set on the key (the key will not exist, because the previous setnx will be automatically created)
      // If this happens, it is that an instance of the process setnx crashes and the subsequent expire is not called
      // At this time, you can directly set expire and use the lock for your own use
      if ($ ttl <0) {
        $ this-> redisString-> set ($ redisKey, $ expireAt);
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      / ***** Circular request lock section ***** /
      // If the waiting time for lock failure is not set or the maximum waiting time has been exceeded, then exit
      if ($ timeout <= 0 || $ timeoutAt <microtime (true)) break;

      // Continue request after $ waitIntervalUs
      usleep ($ waitIntervalUs);

    }

    return false;
  }

  / **
   * Unlock
   * @param [type] $ name [description]
   * @return [type] [description]
   * /
  public function unlock ($ name) {
    // First determine if this lock exists
    if ($ this-> isLocking ($ name)) {
      // Delete the lock
      if ($ this-> redisString-> deleteKey ("Lock: $ name")) {
        // Clear the lock flag in lockedNames
        unset ($ this-> lockedNames [$ name]);
        return true;
      }
    }
    return false;
  }

  / **
   * Release all currently acquired locks
   * @return [type] [description]
   * /
  public function unlockAll () {
    // This flag is used to indicate whether all locks were successfully released
    $ allSuccess = true;
    foreach ($ this-> lockedNames as $ name => $ expireAt) {
      if (false === $ this-> unlock ($ name)) {
        $ allSuccess = false;
      }
    }
    return $ allSuccess;
  }

  / **
   * Increase the specified survival time to the current, must be greater than 0
   * @param [type] $ name [description]
   * @return [type] [description]
   * /
  public function expire ($ name, $ expire) {
    // First determine whether the lock exists
    if ($ this-> isLocking ($ name)) {
      // The specified survival time must be greater than 0
      $ expire = max ($ expire, 1);
      // Increase lock lifetime
      if ($ this-> redisString-> expire ("Lock: $ name", $ expire)) {
        return true;
      }
    }
    return false;
  }

  / **
   * Determine if you currently have a place with the specified name
   * @param [type] $ name [description]
   * @return boolean [description]
   * /
  public function isLocking ($ name) {
    // First look at lonkedName [$ name] if the lock flag name exists
    if (isset ($ this-> lockedNames [$ name])) {
      // Return the lifetime of the lock from redis
      return (string) $ this-> lockedNames [$ name] = (string) $ this-> redisString-> get ("Lock: $ name");
    }

    return false;
  }

}

(B) Code Analysis of Task Queue with Redis

(1) Task queue, which is used to put operations that can be processed asynchronously in the business logic into the queue, and dequeue after processing in other threads

(2) Distributed locks and other logic are used in the queue to ensure the consistency of enqueue and dequeue

(3) This queue is not the same as the ordinary queue. The id when enqueuing is used to distinguish repeated enqueues. There will be only one record in the queue. The same id will overwrite the previous entry instead of appending. Requires repeated enqueuing as unused tasks, please use different ids to distinguish

First look at the code analysis of the enqueue: of course, the validity of the parameters is checked first, and then the content of the locking mechanism above is used, that is, the lock is started. When I enqueue, I choose the current timestamp as the score, and then enter The team is using the add () method of the zset data structure. After the enqueuing is completed, the task is unlocked, and the enqueuing operation is completed.
/ **
   * Join a Task
   * @param [type] $ name queue name
   * @param [type] $ id task id (or its array)
   * @param integer $ timeout enqueue timeout (seconds)
   * @param integer $ afterInterval [description]
   * @return [type] [description]
   * /
  public function enqueue ($ name, $ id, $ timeout = 10, $ afterInterval = 0) {
    // Validity check
    if (empty ($ name) || empty ($ id) || $ timeout <= 0) return false;

    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: {$ name}", $ timeout)) {
      Logger :: get ('queue')-> error ("enqueue faild becouse of lock failure: name = $ name, id = $ id");
      return false;
    }
    
    // When joining the team, use the current timestamp as the score
    $ score = microtime (true) + $ afterInterval;
    // Entry
    foreach ((array) $ id as $ item) {
      // First determine whether the id already exists
      if (false === $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ item)) {
        $ this-> _ redis-> zset-> add ("Queue: $ name", $ score, $ item);
      }
    }
    
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return true;

  }

Next, look at the analysis of the code for dequeuing: To dequeue a task, you need to specify its $ id and $ score. If $ score matches the queue, dequeue, otherwise the task is considered to have been requeued, and the current operation Handle as failure. First, check the validity of the parameters, and then use the lock function, and then leave the team in time. First use getScore () to get the score of the id from Redis, and then pass the incoming $ score and Redis The stored scores are compared, and if the two are equal, the dequeue operation is performed. That is, the delete () method in zset is used to delete the task id, and finally it is unlocked. This is the code analysis of the team.
/ **
   * To dequeue a task, you need to specify $ id and $ score
   * If $ score matches the queue, it will dequeue, otherwise the task will be considered to have been requeued, and the current operation will be handled as failure.
   *
   * @param [type] $ name queue name
   * @param [type] $ id task ID
   * @param [type] $ score The task corresponds to the score. When the task is obtained from the queue, a score will be returned. Only when $ score matches the value in the queue will the task be dequeued.
   * @param integer $ timeout timeout (seconds)
   * @return [type] Whether the task is successful. If false is returned, the redis operation may fail, or $ score does not match the value in the queue.
   * /
  public function dequeue ($ name, $ id, $ score, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || empty ($ id) || empty ($ score)) return false;
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name", $ timeout)) {
      Logger: get ('queue')-> error ("dequeue faild becouse of lock lailure: name = $ name, id = $ id");
      return false;
    }
    
    // dequeue
    // Remove the score of redis first
    $ serverScore = $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ id);
    $ result = false;
    // First determine whether the score passed in is the same as the score of redis
    if ($ serverScore == $ score) {
      // delete the $ id
      $ result = (float) $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
      if ($ result == false) {
        Logger :: get ('queue')-> error ("dequeue faild because of redis delete failure: name = $ name, id = $ id");
      }
    }
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ result;
  }

Anyone who has studied the course of data structure should know that the queue operation also has a method to pop a certain value at the top, etc. Here, the enqueue and dequeue operation is handled here. I also implemented the task of getting several tasks at the top of the queue and dequeue them. Method, friends who want to know can look at this code, if you do not understand it, leave a message, I will not analyze it here.
/ **
   * Get several tasks at the top of the queue and dequeue them
   * @param [type] $ name queue name
   * @param integer $ count quantity
   * @param integer $ timeout timeout
   * @return [type] returns the array [0 => ['id' =>, 'score' =>], 1 => ['id' =>, 'score' =>], 2 => ['id' =>, 'score' =>]]
   * /
  public function pop ($ name, $ count = 1, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || $ count <= 0) return [];
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name")) {
      Log :: get ('queue')-> error ("pop faild because of pop failure: name = $ name, count = $ count");
      return false;
    }
    
    // Remove several Tasks
    $ result = [];
    $ array = $ this-> _ redis-> zset-> getByScore ("Queue: $ name", false, microtime (true), true, false, [0, $ count]);

    // Place it in the $ result array and delete the id corresponding to redis
    foreach ($ array as $ id => $ score) {
      $ result [] = ['id' => $ id, 'score' => $ score];
      $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
    }

    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ count == 1? (empty ($ result)? false: $ result [0]): $ result;
  }

The above is the summary and sharing of the entire set of ideas and code implementation using Redis to implement the task queue. Here I attach the code of an implementation class. I have basically commented each line in the code for everyone to quickly understand and can Simulation applications. If you want to learn more, please look at the code of the entire class:
/ **
 * Task queue
 *
 * /
class RedisQueue {
  private $ _redis;

  public function __construct ($ param = null) {
    $ this-> _ redis = RedisFactory :: get ($ param);
  }

  / **
   * Join a Task
   * @param [type] $ name queue name
   * @param [type] $ id task id (or its array)
   * @param integer $ timeout enqueue timeout (seconds)
   * @param integer $ afterInterval [description]
   * @return [type] [description]
   * /
  public function enqueue ($ name, $ id, $ timeout = 10, $ afterInterval = 0) {
    // Validity check
    if (empty ($ name) || empty ($ id) || $ timeout <= 0) return false;

    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: {$ name}", $ timeout)) {
      Logger :: get ('queue')-> error ("enqueue faild becouse of lock failure: name = $ name, id = $ id");
      return false;
    }
    
    // When joining the team, use the current timestamp as the score
    $ score = microtime (true) + $ afterInterval;
    // Entry
    foreach ((array) $ id as $ item) {
      // First determine whether the id already exists
      if (false === $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ item)) {
        $ this-> _ redis-> zset-> add ("Queue: $ name", $ score, $ item);
      }
    }
    
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return true;

  }

  / **
   * To dequeue a task, you need to specify $ id and $ score
   * If $ score matches the queue, it will dequeue, otherwise the task will be considered to have been requeued, and the current operation will be handled as failure.
   *
   * @param [type] $ name queue name
   * @param [type] $ id task ID
   * @param [type] $ score The task corresponds to the score. When the task is obtained from the queue, a score will be returned. Only when $ score matches the value in the queue will the task be dequeued.
   * @param integer $ timeout timeout (seconds)
   * @return [type] Whether the task is successful. If false is returned, the redis operation may fail, or $ score does not match the value in the queue.
   * /
  public function dequeue ($ name, $ id, $ score, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || empty ($ id) || empty ($ score)) return false;
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name", $ timeout)) {
      Logger: get ('queue')-> error ("dequeue faild becouse of lock lailure: name = $ name, id = $ id");
      return false;
    }
    
    // dequeue
    // Remove the score of redis first
    $ serverScore = $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ id);
    $ result = false;
    // First determine whether the score passed in is the same as the score of redis
    if ($ serverScore == $ score) {
      // delete the $ id
      $ result = (float) $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
      if ($ result == false) {
        Logger :: get ('queue')-> error ("dequeue faild because of redis delete failure: name = $ name, id = $ id");
      }
    }
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ result;
  }

  / **
   * Get several tasks at the top of the queue and dequeue them
   * @param [type] $ name queue name
   * @param integer $ count quantity
   * @param integer $ timeout timeout
   * @return [type] returns the array [0 => ['id' =>, 'score' =>], 1 => ['id' =>, 'score' =>], 2 => ['id' =>, 'score' =>]]
   * /
  public function pop ($ name, $ count = 1, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || $ count <= 0) return [];
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name")) {
      Logger :: get ('queue')-> error ("pop faild because of pop failure: name = $ name, count = $ count");
      return false;
    }
    
    // Remove several Tasks
    $ result = [];
    $ array = $ this-> _ redis-> zset-> getByScore ("Queue: $ name", false, microtime (true), true, false, [0, $ count]);

    // Place it in the $ result array and delete the id corresponding to redis
    foreach ($ array as $ id => $ score) {
      $ result [] = ['id' => $ id, 'score' => $ score];
      $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
    }

    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ count == 1? (empty ($ result)? false: $ result [0]): $ result;
  }

  / **
   * Get several tasks at the top of the queue
   * @param [type] $ name queue name
   * @param integer $ count quantity
   * @return [type] return array [0 => ['id' =>, 'score' =>], 1 => ['id' =>, 'score' =>], 2 => ['id' =>, 'score' =>]]
   * /
  public function top ($ name, $ count = 1) {
    // Validity check
    if (empty ($ name) || $ count <1) return [];

    // take several Tasks
    $ result = [];
    $ array = $ this-> _ redis-> zset-> getByScore ("Queue: $ name", false, microtime (true), true, false, [0, $ count]);
    
    // Store Task in an array
    foreach ($ array as $ id => $ score) {
      $ result [] = ['id' => $ id, 'score' => $ score];
    }

    // return the array
    return $ count == 1? (empty ($ result)? false: $ result [0]): $ result;
  }
}

At this point, the two major functions are basically explained. For the task queue, you can write a shell script to let the server run certain programs at regular intervals to implement operations such as enqueuing and dequeuing. I will not combine it with actual applications here. Go to achieve, you can understand the implementation ideas of these two functions, because the code is written in PHP language, if you understand the implementation ideas, you can use java or .net and other languages to implement Both functions. There are many application scenarios for these two functions, especially spike, and the other is to grab train tickets during the Spring Festival. These two are the most obvious examples. Of course, there are many places to use, I will not list them here one by one.

Well, this concludes the sharing and sharing. Finally, I attach two classes of distributed locks and task queues:
/ **
 * Implement distributed locks on redis
 * /
class RedisLock {
  private $ redisString;
  private $ lockedNames = [];

  public function __construct ($ param = NULL) {
    $ this-> redisString = RedisFactory :: get ($ param)-> string;
  }

  / **
   * Locked
   * @param [type] $ name the distinguished name of the lock
   * @param integer $ timeout Wait for the lock to time out in a loop. During this time, it will try to acquire the lock until it times out. If it is 0, it means to return directly without waiting.
   * @param integer $ expire The maximum lifetime (seconds) of the current lock, which must be greater than 0. If the lock has not been released beyond the lifetime, the system will automatically force the release
   * @param integer $ waitIntervalUs Interval (in microseconds) to suspend and retry after acquiring a lock failure
   * @return [type] [description]
   * /
  public function lock ($ name, $ timeout = 0, $ expire = 15, $ waitIntervalUs = 100000) {
    if ($ name == null) return false;

    // Get the current time
    $ now = time ();
    // Wait timeout when lock acquisition fails
    $ timeoutAt = $ now + $ timeout;
    // The maximum survival time of the lock
    $ expireAt = $ now + $ expire;

    $ redisKey = "Lock: {$ name}";
    while (true) {
      // Save the maximum survival time of rediskey in redis. After this time, the lock will be automatically released.
      $ result = $ this-> redisString-> setnx ($ redisKey, $ expireAt);

      if ($ result! = false) {
        // Set the key expiration time
        $ this-> redisString-> expire ($ redisKey, $ expireAt);
        // Place the lock flag in the lockedNames array
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      // Returns the remaining survival time of the given key in seconds
      $ ttl = $ this-> redisString-> ttl ($ redisKey);

      // ttl is less than 0, which means no time to live is set on the key (the key will not exist, because the previous setnx will be automatically created)
      // If this happens, it is that an instance of the process setnx crashes and the subsequent expire is not called
      // At this time, you can directly set expire and use the lock for your own use
      if ($ ttl <0) {
        $ this-> redisString-> set ($ redisKey, $ expireAt);
        $ this-> lockedNames [$ name] = $ expireAt;
        return true;
      }

      / ***** Circular request lock section ***** /
      // If the waiting time for lock failure is not set or the maximum waiting time has been exceeded, then exit
      if ($ timeout <= 0 || $ timeoutAt <microtime (true)) break;

      // Continue request after $ waitIntervalUs
      usleep ($ waitIntervalUs);

    }

    return false;
  }

  / **
   * Unlock
   * @param [type] $ name [description]
   * @return [type] [description]
   * /
  public function unlock ($ name) {
    // First determine if this lock exists
    if ($ this-> isLocking ($ name)) {
      // Delete the lock
      if ($ this-> redisString-> deleteKey ("Lock: $ name")) {
        // Clear the lock flag in lockedNames
        unset ($ this-> lockedNames [$ name]);
        return true;
      }
    }
    return false;
  }

  / **
   * Release all currently acquired locks
   * @return [type] [description]
   * /
  public function unlockAll () {
    // This flag is used to indicate whether all locks were successfully released
    $ allSuccess = true;
    foreach ($ this-> lockedNames as $ name => $ expireAt) {
      if (false === $ this-> unlock ($ name)) {
        $ allSuccess = false;
      }
    }
    return $ allSuccess;
  }

  / **
   * Increase the specified survival time to the current, must be greater than 0
   * @param [type] $ name [description]
   * @return [type] [description]
   * /
  public function expire ($ name, $ expire) {
    // First determine whether the lock exists
    if ($ this-> isLocking ($ name)) {
      // The specified survival time must be greater than 0
      $ expire = max ($ expire, 1);
      // Increase lock lifetime
      if ($ this-> redisString-> expire ("Lock: $ name", $ expire)) {
        return true;
      }
    }
    return false;
  }

  / **
   * Determine if you currently have a place with the specified name
   * @param [type] $ name [description]
   * @return boolean [description]
   * /
  public function isLocking ($ name) {
    // First look at lonkedName [$ name] if the lock flag name exists
    if (isset ($ this-> lockedNames [$ name])) {
      // Return the lifetime of the lock from redis
      return (string) $ this-> lockedNames [$ name] = (string) $ this-> redisString-> get ("Lock: $ name");
    }

    return false;
  }

}

/ **
 * Task queue
 * /
class RedisQueue {
  private $ _redis;

  public function __construct ($ param = null) {
    $ this-> _ redis = RedisFactory :: get ($ param);
  }

  / **
   * Join a Task
   * @param [type] $ name queue name
   * @param [type] $ id task id (or its array)
   * @param integer $ timeout enqueue timeout (seconds)
   * @param integer $ afterInterval [description]
   * @return [type] [description]
   * /
  public function enqueue ($ name, $ id, $ timeout = 10, $ afterInterval = 0) {
    // Validity check
    if (empty ($ name) || empty ($ id) || $ timeout <= 0) return false;

    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: {$ name}", $ timeout)) {
      Logger :: get ('queue')-> error ("enqueue faild becouse of lock failure: name = $ name, id = $ id");
      return false;
    }
    
    // When joining the team, use the current timestamp as the score
    $ score = microtime (true) + $ afterInterval;
    // Entry
    foreach ((array) $ id as $ item) {
      // First determine whether the id already exists
      if (false === $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ item)) {
        $ this-> _ redis-> zset-> add ("Queue: $ name", $ score, $ item);
      }
    }
    
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return true;

  }

  / **
   * To dequeue a task, you need to specify $ id and $ score
   * If $ score matches the queue, it will dequeue, otherwise the task will be considered to have been requeued, and the current operation will be handled as failure.
   *
   * @param [type] $ name queue name
   * @param [type] $ id task ID
   * @param [type] $ score The task corresponds to the score. When the task is obtained from the queue, a score will be returned. Only when $ score matches the value in the queue will the task be dequeued.
   * @param integer $ timeout timeout (seconds)
   * @return [type] Whether the task is successful. If false is returned, the redis operation may fail, or $ score does not match the value in the queue.
   * /
  public function dequeue ($ name, $ id, $ score, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || empty ($ id) || empty ($ score)) return false;
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name", $ timeout)) {
      Logger: get ('queue')-> error ("dequeue faild becouse of lock lailure: name = $ name, id = $ id");
      return false;
    }
    
    // dequeue
    // Remove the score of redis first
    $ serverScore = $ this-> _ redis-> zset-> getScore ("Queue: $ name", $ id);
    $ result = false;
    // First determine whether the score passed in is the same as the score of redis
    if ($ serverScore == $ score) {
      // delete the $ id
      $ result = (float) $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
      if ($ result == false) {
        Logger :: get ('queue')-> error ("dequeue faild because of redis delete failure: name = $ name, id = $ id");
      }
    }
    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ result;
  }

  / **
   * Get several tasks at the top of the queue and dequeue them
   * @param [type] $ name queue name
   * @param integer $ count quantity
   * @param integer $ timeout timeout
   * @return [type] returns the array [0 => ['id' =>, 'score' =>], 1 => ['id' =>, 'score' =>], 2 => ['id' =>, 'score' =>]]
   * /
  public function pop ($ name, $ count = 1, $ timeout = 10) {
    // Validity check
    if (empty ($ name) || $ count <= 0) return [];
    
    // Lock
    if (! $ this-> _ redis-> lock-> lock ("Queue: $ name")) {
      Logger :: get ('queue')-> error ("pop faild because of pop failure: name = $ name, count = $ count");
      return false;
    }
    
    // Remove several Tasks
    $ result = [];
    $ array = $ this-> _ redis-> zset-> getByScore ("Queue: $ name", false, microtime (true), true, false, [0, $ count]);

    // Place it in the $ result array and delete the id corresponding to redis
    foreach ($ array as $ id => $ score) {
      $ result [] = ['id' => $ id, 'score' => $ score];
      $ this-> _ redis-> zset-> delete ("Queue: $ name", $ id);
    }

    // Unlock
    $ this-> _ redis-> lock-> unlock ("Queue: $ name");

    return $ count == 1? (empty ($ result)? false: $ result [0]): $ result;
  }

  / **
   * Get several tasks at the top of the queue
   * @param [type] $ name queue name
   * @param integer $ count quantity
   * @return [type] return array [0 => ['id' =>, 'score' =>], 1 => ['id' =>, 'score' =>], 2 => ['id' =>, 'score' =>]]
   * /
  public function top ($ name, $ count = 1) {
    // Validity check
    if (empty ($ name) || $ count <1) return [];

    // take several Tasks
    $ result = [];
    $ array = $ this-> _ redis-> zset-> getByScore ("Queue: $ name", false, microtime (true), true, false, [0, $ count]);
    
    // Store Task in an array
    foreach ($ array as $ id => $ score) {
      $ result [] = ['id' => $ id, 'score' => $ score];
    }

    // return the array
    return $ count == 1? (empty ($ result)? false: $ result [0]): $ result;
  }
}
The above is the entire content of this article, I hope it will be helpful to everyone's learning.



Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.