分布式锁的几种实现方式学习

1 简介

在分布式环境中,用来正确操作互斥的共享资源。它需要保证两个特点。

  • 锁超时:确保持有锁的线程出现问题时,能够及时释放,不会被永久占用。
  • 排他性:一个锁仅能被一个线程拥有。

2 基于关系数据库实现

以mysql为例,利用mysql的唯一键冲突以及行锁的功能来实现

2.1 唯一键实现

排他性:维护一张数据表,并有唯一的字段。表中每条记录表示申请的锁。当获取锁时,向表中新增一条数据。如果成功新增,增表示加锁成功。若出现唯一键冲突,说明锁已被占用。

超时:记录中增加超时时间,通过定时任务定时扫描表内记录,当出现超过阈值还未被删除时,则定时任务删除该记录。

问题:如果任务执行时间过长,锁持有时间超过设置的超时时间,那么定时任务会误删除该锁,导致锁被错误释放。

2.2 加锁的实现

排他性:获取锁时增加一条记录表示该锁,如果添加成功尝试加锁。例如通过select for update加锁。如果增加失败,则返回。

超时:当客户端宕机,无响应,通过数据库超时机制[1],保证锁的释放。通过show global variables like "%timeout%";查看超时配置。

问题:如果获取锁的任务处理时间过长,那么会导致大量连接被占用。

3 基于Redis实现

利用redis集群及redis提供的互斥方法实现。

3.1 基于SetNx方法

排他性:setnx加锁,如果设置成功,表示加锁成功,反之表示加锁失败。
锁超时:通过redis的超时功能,到达超时时间后redis会对过期的key清理,最终锁释放。
问题

  • 任务自身执行时间过长,超过超时时间,最终被误删。
    此外如果超时时间设置的过长,客户端本身发生了宕机。这样其它客户端需要等待较长的时间获取锁。这是在设置超时时间时,可以先制定一个较短的时间,随后在使用过程中,再重新刷新超时时间,直到最终释放。这样即使出现宕机的情况,其它客户端也可以较快的获取到锁。
  • 如果在获取数据过程中,集群数据不一致。例如主从切换,那么将导致错误释放。redis本身并未保证强一致,因为会产生问题。

3.2 RedLock方法

排他性:3.1的方法如果出现数据不一致的情况,获取锁时,连接到多个redis主节点上。加锁时,向所有主节点发起请求。当得到大多数节点返回成功时,任务加锁成功[2]。

锁超时:通过redis的超时功能,到达超时时间后redis会对过期的key清理,最终锁释放。
问题

  • 请求多个主节点,性能会有所降低
  • 任务时间太长,导致锁被误删,进入错误的释放状态。
  • redlock的其它问题[5]

4 基于一致性协议

基于一致性的实现可以使用使用ZooKeeper、etcd或者实现Raft算法,本次学习中主要学习zookeeper的实现。

4.1 通过Zookeeper临时结点基础实现

排他性:[3]

  • 户端调用方法创建名为“locknode/guid-lock-”的节点,需要注意的是,节点的创建类型未临时顺序结点
  • 客户端调用getChildren(“locknode”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher
  • 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
  • 如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。

锁超时:如果客户端宕机,会话超时,那么临时结点被删除,实现超时功能。
问题:当有大量客户端通过该方法等待锁,当结点发生变更,会有大量的客户端同时向服务器发起请求,尝试获取锁,对服务器造成冲击,这就是这个场景下的羊群效应[4]。仔细考虑发现,监听结点变更,只需要关注需要必自己小的结点状态即可。这样最终减少结点变更时,通知客户端的数量。

4.2 仅关注上一个结点的变更

根据4.1中问题的原因,对排他性的实现进行改进。

  1. 新建“/locknode/lock-“的临时顺序结点节点。
  2. 客户端在路径“/lockNode/”调用getChildren(),获取已创建的子结点
  3. 如果所创建的子结点在所有子结点列表中是最小节点(拥有最小数字后缀)说明获取锁成功,结束并返回。
  4. 如果不是最小节点,使用exists()在后缀数值仅次于当前的节点加Watcher监听器。
  5. 随后这个被监听的结点被删除后,客户端收到通知。再次调用getChildren方法获取子结点列表。

5 开源工具

  • Apache Helix :基于一致性协议的集群资源管理框架,并提供了分布式锁功能。与ZooKeeper实现方式相比,Apache Helix进一步实现了公平的锁获取机制。
  • Redisson:基于Redis实现分布式锁。
  • Menagerie:基于ZooKeeper实现。
  • Curator:对ZooKeeper的客户端封装,其分布式锁的实现基于ZooKeeper完成。在ZooKeeper创建EPHEMERAL_SEQUENTIAL节点实现加锁。临时结点保证了锁持有者与ZooKeeper断开时释放锁;节点的顺序性特性避免了加锁较多时的羊群效应。

6 学习总结

基于kv数据库及一致性框架,在分布式环境下,单点化操作并通过一致性机制保证可靠性。基于kv和一致性框架的实现各有其优缺点。如果考虑性能,可以考虑使用基于kv实现的框架。如果是追求强一致性,则可靠使用基于一致性框架的实现(zk,etcd)。

[1]mysql超时类型,https://zhuanlan.zhihu.com/p/50739231
[2]redlock,https://redis.io/topics/distlock
[3]zookeeper分布式锁,https://www.cnblogs.com/codershuai/p/4182441.html
[4]羊群效应,https://baike.baidu.com/item/%E7%BE%8A%E7%BE%A4%E6%95%88%E5%BA%94/850648?fr=aladdin
[5]redlock真的可靠么,https://zhuanlan.zhihu.com/p/41327417

你可能感兴趣的:(系统)