分布式锁实现

什么时候需要加锁?

  • 资源是共享的
  • 资源是互斥的
  • 多任务环境



为什么需要分布式锁?传统的多线程解决方案为什么适用了?

分布式事务,往往都是多个不同的主机并发的,他们的主机上有着不同的JVM环境,Java多线程加锁对象是对对象对类的线程控制,只能阻止同一个JVM内的资源被JVM内的线程同时占用的情况。而不同的JVM需要互斥占用一个外部的资源时,就需要分布式锁的实现了。



基于MySQL实现分布式锁

浅析分布式锁实现

实现思路
  • MySQL中的设置一个字段
  • 上锁:当需要进行加锁时,首先需要先去MySQL抢夺字段,将自己线程名写到该字段上,一旦该字段上已经被其他线程修改了,则代表上锁
  • 执行事务逻辑:进行一系列事务操作
  • 解锁:该线程将该字段上的信息恢复原来的未加锁状态,此时其他线程可以对此锁进行竞争
存在问题
  • 浪费资源:若线程没有设置休眠时间,不停地去查看该字段进行SELECT操作,浪费了资源
    • 解决方案?若设置了休眠时间来减少SELECT次数,则线程取得锁的反应不够及时
  • 死锁问题:在线程A获得了该锁后,突然执行程序错误或者宕机了,导致后续操作无法完成,该字段无法被恢复
    • 解决方案?若引入监视器来监视该字段,让在超过超时时间后,恢复无锁状态,则出现新的问题:
      • 监视器宕机?线程同时无锁?->引入多个监视器,监视器数据一致性?脑裂
      • 超时时间设置多久合适?若太短,可能导致乱入锁问题;若太长,导致资源被浪费,反应不够及时


基于Redis实现分布式锁

浅析分布式锁实现

实现思路
  • Redis中的设置一个Key
  • 上锁:当需要进行加锁时,首先需要先去判断Redis中是否存在Key,将自己线程名写到该Key上,一旦该Key上已经被其他线程修改了,则代表上锁
  • 执行事务逻辑:进行一系列事务操作
  • 解锁:该线程将该Key删除或将信息恢复原来的未加锁状态,此时其他线程可以对此锁进行竞争

存在问题

  • 超时时间设置时长问题:若由于系统GC等原因导致逻辑未执行完就被解锁,从而出现乱入锁问题
  • 主从复制时:线程从Master成功写入后,获得锁,Master宕机,数据丢失,未被同步,从而导致乱入锁问题,针对此问题,Redis作者提出RedLock算法


### RedLock算法

Redis作者提出的,为了解决(减少概率)主从复制时Master宕机导致数据未同步出现的乱入锁问题

官方文档

假设我们的Redis集群中有N个Master(N >= 3)

  • 获取当前时间(毫秒ms)

  • 当需要去获得锁时,需要轮流用相同的Key与随机值在N个Master上请求锁,在这一步里,我们请求的在每个Master请求锁时,设置的过期时间是比原来的设置的小得多的值,(例如:原来设置的10s,这一步,我们只需设置5ms-50ms即可)若某一个Master宕机,我们应该尽快尝试下一个结点

  • 轮询完毕后,统计成功设置的Master次数是否超过半数,并且花费的时间未超过原来设置的超时时间则为获取锁成功了

  • 如果锁获取成功,则超时时间则设置为原来的超时时间减去获取锁的时间

  • 如果锁获取失败,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个Master节点上释放锁,即便是那些他认为没有获取成功的锁(这边未能理解,查看官方文档

存在问题:

  • 节点崩溃重启,会出现多个客户端持有锁
    假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
    (1)客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
    (2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
    (3)节点C重启后,客户端2锁住了C, D, E,获取锁成功。
    这样,客户端1和客户端2同时获得了锁(针对同一资源)。

为了应对节点重启引发的锁失效问题,redis的作者antirez提出了延迟重启的概念,即一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,等待的时间大于锁的有效时间。采用这种方式,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。这其实也是通过人为补偿措施,降低不一致发生的概率。

  • 时间跳跃问题
    (1)假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
    (2)客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。
    (3)节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。
    客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。
    客户端1和客户端2现在都认为自己持有了锁。

为了应对始终跳跃引发的锁失效问题,redis的作者antirez提出了应该禁止人为修改系统时间,使用一个不会进行“跳跃”式调整系统时钟的ntpd程序。这也是通过人为补偿措施,降低不一致发生的概率。

  • 超时导致锁失效问题

    超时时间的设置时长问题,Redis只能确保不拿到一个已经过期的锁,却不能保证在执行逻辑的过程中,锁失效的问题。



基于ZooKeeper实现分布式锁

浅析分布式锁实现

实现思路
  • 增加结点:每个需要去获得这把分布式锁的线程一上来先到zkClient上注册个有序的临时结点znode
  • 添加监听:若发现该同名有序结点前仍有结点znode存在,去对前一个结点znode设置监听事件,并把需要执行的逻辑处理,放在监听的回调函数
  • 断开连接,删除结点:由于zk临时结点znode会随着客户端断开连接而被自动删除,且借助ZAB协议,同步到每一个zkClient
  • 触发监听回调,执行逻辑:在前面的结点znode一消失(证明前一个事务执行完成,锁释放),就会马上调用下一个需要执行的逻辑处理

存在问题

  • 性能较差
  • 在上锁之后,执行逻辑的过程中,系统突然发生GC,导致与zk集群的连接心跳超时,锁的状态被取消,导致乱入锁问题

相关文章: