RedLock.php 3.3 KB

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