分布式锁一般用 setnx(set if not exists) 命令,只允许被一个客户端获得锁,先到先得,用完后再用 del 释放锁。

版本1:

  1. # 获取锁
  2. 127.0.0.1:6379> setnx lock:temp random
  3. (integer) 1
  4. # ---- 如果得到锁,执行业务代码 ----
  5. # ---- 如果没有得到锁,继续尝试获取锁 ----
  6. # 释放锁
  7. 127.0.0.1:6379> del lock:temp
  8. (integer) 1

这样就会有一个问题,如果业务代码执行出错没有被捕捉到,或者由于某种特殊情况程序中断,就会导致 del 命令没有执行到,这样就会陷入死锁,锁永远得不到释放。

版本2:

  1. # 获取锁
  2. 127.0.0.1:6379> setnx lock:temp random
  3. (integer) 1
  4. # 设置锁的过期时间
  5. 127.0.0.1:6379> expire lock:temp 5
  6. (integer) 1
  7. # ---- 如果得到锁,执行业务代码 ----
  8. # ---- 如果没有得到锁,继续尝试获取锁 ----
  9. # 释放锁
  10. 127.0.0.1:6379> del lock:temp
  11. (integer) 1

版本 2 增加了锁的过期时间,这样出现异常没有正常释放锁的话可以保证一断时间后自动释放锁。但版本 2 还是有一个问题,如果在 setnxexpire 之间服务器出现异常,导致 expire 命令没有执行到,也会出现死锁问题。因为 setnxexpire 是两条分开执行的指令,不是原子操作。
还有一个问题,如果 setnx 没有拿到锁,expire 是不应该执行的。

版本3:

  1. # 获取锁
  2. 127.0.0.1:6379> set lock:temp random ex 5 nx
  3. OK
  4. # ---- 如果得到锁,执行业务代码 ----
  5. # ---- 如果没有得到锁,继续尝试获取锁 ----
  6. # 释放锁
  7. 127.0.0.1:6379> del lock:temp
  8. (integer) 1

版本 3 使用新的命令 set {key} {value} ex {seconds} nx 解决操作原子性问题。该命令能保证获取锁与设置超时时间操作的原子性。
但是该版本还有问题:

  1. 如果 A 请求业务代码执行时间超过锁的时间,这时锁会自动释放,这时假如 B 得到锁,B 请求刚得到锁的时候,A 请求的业务代码执行完了,A 请求调用了 del 释放锁,这样就会出问题,因为 A 释放的不是自己的锁。
  2. 如果业务代码出现异常没有被捕获,程序运行就中止了,这样就会导致锁得不到释放,所以在执行业务代码的时候要增加 try catch。

    版本4:

    ```shell

    获取锁

    127.0.0.1:6379> set lock:temp random ex 5 nx OK

—— 如果得到锁,执行业务代码,注意要捕获业务代码的异常 ——

—— 如果没有得到锁,继续尝试获取锁 ——

先获取存入的随机数

127.0.0.1:6379> get lock:temp “random”

情况1:get 命令获取的随机数与存入的随机数一致,说明当前请求没有超时,调用 del 释放锁

127.0.0.1:6379> del lock:temp (integer) 1

情况2:get 命令获取的随机数与存入的随机数不一致,说明当前请求业务代码执行时长超过锁有效时长,

锁已经提前自动释放,所以不用调用 del

  1. <a name="ocvSP"></a>
  2. ## PHP 分布式锁代码实现:
  3. ```php
  4. /**
  5. * 分布式锁模板方法,如果获取不到锁则再次尝试
  6. * @param string $key 锁的对象
  7. * @param Closure $handle 回调方法(业务方法)
  8. * @return mixed 业务方法返回值
  9. * @throws Exception
  10. */
  11. static public function distributedLockTemplate($key, Closure $handle)
  12. {
  13. // 锁有效时长
  14. $time = 5;
  15. // 存入随机数,防止业务代码超时释放锁出错
  16. $random = GUIDService::getGuid();
  17. // 获取锁
  18. $count = self::setExNx($key, $random, $time);
  19. // 记录返回值
  20. $result = null;
  21. // 如果获取到锁
  22. if ($count > 0) {
  23. // 执行业务代码
  24. try {
  25. $result = $handle();
  26. } catch (Exception $exception) {
  27. self::releaseLock($key, $random);
  28. throw $exception;
  29. }
  30. self::releaseLock($key, $random);
  31. } else {
  32. // 如果没有获取到锁,再次尝试获取
  33. sleep(0.2);
  34. // 递归调用
  35. $result = self::distributedLockTemplate($key, $handle);
  36. }
  37. return $result;
  38. }
  39. /**
  40. * 释放锁
  41. * @param $key
  42. * @param $random
  43. */
  44. static private function releaseLock($key, $random)
  45. {
  46. // 获取随机数,防止业务代码超时释放锁出错
  47. $catchRandom = self::get($key);
  48. // 对比随机数
  49. if ($random == $catchRandom) {
  50. // 释放锁
  51. RedisService::del($key);
  52. }
  53. }
  54. static public function get($key)
  55. {
  56. return Redis::get($key);
  57. }
  58. static public function setExNx($key, $value, $second)
  59. {
  60. return Redis::set($key, $value, 'ex', $second, 'nx');
  61. }
  • 获取不到锁的多种处理方式
  1. 递归调用重新获取
  2. 抛异常给用户,提示重试
  3. 放得任务队列重试