RedLock.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. <?php
  2. declare (strict_types = 1);
  3. class RedLock
  4. {
  5. private $retryDelay;
  6. private $retryCount;
  7. private $clockDriftFactor = 0.01;
  8. private $quorum;
  9. private $servers = array();
  10. private $instances = array();
  11. function __construct( $retryDelay = 200, $retryCount = 3)
  12. {
  13. $this->servers = [
  14. [
  15. 'host' => env('redis.host','127.0.0.1'),
  16. 'port' => env('redis.port', 6379),
  17. 'password' => env('redis.password', '123456'),
  18. 'select' => env('redis.select', 10),
  19. 'timeout' => env('redis.timeout', 1000),
  20. ]
  21. ];
  22. $this->retryDelay = $retryDelay;
  23. $this->retryCount = $retryCount;
  24. $this->quorum = min(count($this->servers), (count($this->servers) / 2 + 1));
  25. }
  26. public function lock($resource, $ttl = 600)
  27. {
  28. $this->initInstances();
  29. $token = uniqid();
  30. $retry = $this->retryCount;
  31. do {
  32. $n = 0;
  33. $startTime = microtime(true) * 1000;
  34. foreach ($this->instances as $instance) {
  35. if ($this->lockInstance($instance, $resource, $token, $ttl)) {
  36. $n++;
  37. }
  38. }
  39. # Add 2 milliseconds to the drift to account for Redis expires
  40. # precision, which is 1 millisecond, plus 1 millisecond min drift
  41. # for small TTLs.
  42. $drift = ($ttl * $this->clockDriftFactor) + 2;
  43. $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
  44. if ($n >= $this->quorum && $validityTime > 0) {
  45. return [
  46. 'validity' => $validityTime,
  47. 'resource' => $resource,
  48. 'token' => $token,
  49. ];
  50. } else {
  51. foreach ($this->instances as $instance) {
  52. $this->unlockInstance($instance, $resource, $token);
  53. }
  54. }
  55. // Wait a random delay before to retry
  56. $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
  57. usleep($delay * 1000);
  58. $retry--;
  59. } while ($retry > 0);
  60. return false;
  61. }
  62. public function unlock(array $lock)
  63. {
  64. $this->initInstances();
  65. $resource = $lock['resource'];
  66. $token = $lock['token'];
  67. foreach ($this->instances as $instance) {
  68. $this->unlockInstance($instance, $resource, $token);
  69. }
  70. }
  71. private function initInstances()
  72. {
  73. if (empty($this->instances)) {
  74. foreach ($this->servers as $server) {
  75. $redis = new \think\cache\driver\Redis($server);
  76. $this->instances[] = $redis;
  77. }
  78. }
  79. }
  80. private function lockInstance($instance, $resource, $token, $ttl)
  81. {
  82. return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
  83. }
  84. private function unlockInstance($instance, $resource, $token)
  85. {
  86. $script = '
  87. if redis.call("GET", KEYS[1]) == ARGV[1] then
  88. return redis.call("DEL", KEYS[1])
  89. else
  90. return 0
  91. end
  92. ';
  93. return $instance->eval($script, [$resource, $token], 1);
  94. }
  95. }