当前位置:   article > 正文

分布式锁的实现(redis)_redis分布式锁的实现

redis分布式锁的实现

1、单机锁

考虑在并发场景并且存在竞态的状况下,我们就要实现同步机制了,最简单的同步机制就是加锁。

加锁可以帮我们锁住资源,如内存中的变量,或者锁住临界区(线程中的一段代码),使得同一个时刻只有一个线程能访问某一个区域。

如果是单实例(单进程部署),那么单机锁就可以满足我们的要求了,如synchronized,ReentrantLock。

因为在一个进程中的不同线程可以共享这个锁。

2、分布式锁

但是如果场景来到了分布式系统呢?

分布式系统部署在不同的机器上,或者只是简单的多进程部署。这样各个进程之间无法共享同一个锁。

这时候我们要加分布式锁

分布式锁大概就是这么一个东西:通过共享的存储缓存一个状态值,用状态值的变化标识锁的占用和释放。

可以通过mysql,redis,zk等实现分布式锁,这里我们实现一个redis的。如果你用java其实使用zk会很简单。

3、为什么redis能用来实现分布式锁?

1)Redis是单进程单线程模式

redis实现为单进程单线程模式,这样多个客户端并不存在竞态关系。

2)原子性原语

redis提供了可以实现原子操作的原语如setnx、getset等。

setnx

  1. 1)SETNX key value
  2. key 的值设为 value ,当且仅当 key 不存在。
  3. 若给定的 key 已经存在,则 SETNX 不做任何动作。
  4. SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
  5. 可用版本:
  6. >= 1.0.0
  7. 时间复杂度:
  8. O(1)
  9. 返回值:
  10. 设置成功,返回 1
  11. 设置失败,返回 0

复制

getset

  1. GETSET key value
  2. 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
  3. key 存在但不是字符串类型时,返回一个错误。
  4. 可用版本:
  5. >= 1.0.0
  6. 时间复杂度:
  7. O(1)
  8. 返回值:
  9. 返回给定 key 的旧值。
  10. key 没有旧值时,也即是, key 不存在时,返回 nil 。

复制

4、实现

  1. package com.xiaoju.dqa.fusor.utils;
  2. import com.xiaoju.dqa.fusor.client.RedisClient;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.stereotype.Component;
  5. @Component
  6. public class DistributeLockUtil {
  7. // 锁超时时间, 防止死锁
  8. private static final long LOCK_TIMEOUT = 60;
  9. @Autowired
  10. private RedisClient redisClient;
  11. private boolean locked = false;
  12. public boolean lock(String key) {
  13. String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
  14. /*
  15. * setnx 返回1
  16. * 说明: 1)key不存在, 2)成功写入锁, 并更新锁的生存时间
  17. * 也就是get
  18. * */
  19. if (redisClient.setnx(key, expireTime) == 1) {
  20. locked = true;
  21. return true;
  22. }
  23. /*
  24. * 没有get锁, 下面进入判断锁超时逻辑
  25. * */
  26. String currentExpireTime = redisClient.get(key);
  27. /*
  28. * 锁生存时间已经过了, 说明锁已经超时
  29. * */
  30. if (Long.parseLong(currentExpireTime) < System.currentTimeMillis()) {
  31. String oldValueStr = redisClient.getSet(key, expireTime);
  32. /*
  33. * 判断锁生存时间和你改的写那个时间是否相等
  34. * 相当于你竞争了一个更新锁
  35. * */
  36. if (oldValueStr.equals(currentExpireTime)) {
  37. locked = true;
  38. return true;
  39. }
  40. }
  41. return false;
  42. }
  43. public void release(String key) {
  44. if (locked) {
  45. redisClient.del(key);
  46. locked = false;
  47. }
  48. }
  49. }

复制

5、死锁

为了解决死锁,这里设置了锁的超时时间。

    private static final long LOCK_TIMEOUT = 60;

复制

并通过setnx时更新锁生存时间来维护锁超时的判定。

  1. String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
  2. ...
  3. if (redisClient.setnx(key, expireTime) == 1) {
  4. ...
  5. }
  6. ...
  7. String oldValueStr = redisClient.getSet(key, expireTime);
  8. ...

复制

为什么要使用这种方式,而不是expire呢?

因为setnx和expire不能作为一个原子性的操作存在,设想如果setnx之后,在执行expire之前出现了异常,那么锁将没有超时时间。也就是死锁。

6、解决锁超时引入的竞态

设想三个客户端,C0,C1,C2

如果C0持有锁并且崩溃,锁没有释放。

C1和C2同时发现了锁超时。

然后都通过getset去拿到了旧值,在对比了旧值和之前值之后,如果相等,那么说明“我”成功修改了旧值,那么我就拿到了锁。

7、 时钟同步

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。 锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

8、一些处理不了的情况

设想三个客户端,C0,C1,C2

如果C0持有锁很长,锁已经超时。这时候有C1,C2判断锁超时了,然后通过超时竞争,C1拿到了锁。

这时C0醒了过来,删除了C1的锁。

这时,C1认为自己独占了锁,其他的进程也进入了竞争锁的情况

对于这种情况,这里是没有提供解决办法的。

思路是:你降级你的锁,比如给你的锁加上uuid,对不同的业务或者不同的session加上对应粒度的锁。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/696706
推荐阅读
相关标签
  

闽ICP备14008679号