关于分布式锁的总结
1. 背景
最近做了一个分布式的调度系统,调度系统是分布式的,有一些任务我希望能做到单个节点去完成,所以就产生了主的概念;也就是多节点选择抢占出来一个节点,这个节点能唯一去做一些事情,当然也希望这个节点故障的时候能重新触发抢占并达到系统的容错性;
自己在设计的时候出现了一种混淆,节点本身的健康度和抢占两个逻辑的混淆,所以想通过这个文章来总结这方面的内容;并且后期会开源一个工具,能依赖各种后端存储来实现一套分布式锁的工具;
2. 分布式锁
对于分布式锁的关键点在于几点:
如何实现抢占: 具体来将就是多方发起请求,能保证原子的抢占成功;也就是最终只会有一方能成功的抢占到;
通常这种能力都是对应后端服务来提供的;比如:
Mongo:findAndModify接口,符合条件才能进行更新,并且可以设置如果不存在可以写入成功;Redis:SETNX lockName true, 能原子的写入某一个key;ZK:ephemeral node,可以原子的创建临时节点ETCD: 提供类似于CAS的方式来进行更新;
失效机制: 这个非常重要,如何保证当一个节点故障的时候,不会因为这个问题导致锁长期被占用;也是提供整个分布式锁的容错性;失效机制通用实现的方式是
Lease,也就是每一个锁都有着占用时间长度,超过这个时间长度就会失效,这样能保证即使节点挂了,过了租期之后就会进行失效,可以保证其他的用户能占用这个锁;用什么方式去实现租期呢,有几种方式:
- 基于mongo服务的类型的话,可以通过设置过期时间;其他的用户在抢占锁的时候通过对过期时间是否过期作为条件能抢占锁,如果锁本身已经过期那么抢占就会成功;
- 基于redis是有一个命令
SET lock_name value NX EX 10, 大概意思是设置一个锁,并且有效期为10秒 ZK: zk的临时节点貌似是基于session的,如果节点挂了session也就没了;ETCD:可以直接设置key的过期时间;
自动续租期: 主要是针对锁的时间长短的问题;比如一个操作可能需要很长的操作时间,并且也不确定要多久,这个时候就可以开启自动续约租期;这样只需要手动进行调用关闭即可;
但是关于这个问题在于:自动续约的功能可能会导致一些其他的问题;比如当占用锁的用户可能因为操作过程因为hang住,并且自动续期又正常工作的话,那就会带来
lock没办法被释放; 关于这点有点很难办,可能需要具体场景具体做分析优化吧;比如在mongo中就遇到过这样的问题,因为心跳链接是提前创建好的,mongo本身因为一些bug已经卡死了,但是心跳还是能正常工作,就导致不会进行主动的切主操作;所以所谓的健康度检测可能还是需要能正式反应业务是上是否正常来评估;
关于这个租期的问题,其实也是我当时混淆进去的一点;我把节点的健康检测和租期续约给混在一起,导致写出来的代码有点混乱;目前我的思路可能是健康检测是健康检测;如果健康检测发现有问题的话,可以重启或者让自动续约的逻辑可以注入是否健康的函数,如果本节点已经不健康了就不进行续约之类的;
锁过期之后的处理
这个比较重要,假如一个程序以为自己占用了一个锁,但是本质上这个锁已经过期了需要如何处理;这其实并不好处理,原因是程序在执行过程中会有很多次停顿,不可能从所有停顿的地方都进行判断,比如java的gc的stop world,等程序从gc中回来的时候它可能根本不知道自己已经不占用锁,也会正常的做操作,这可能就是分布式带来的问题;如果是淡单机的lock就不会有这样的问题,因为单机本身lock是不会有所谓的过期,完全保证原子性,而分布式只能通过超时的方式来处理lock的有效性问题;
我对这块的处理是这样的;
- 分布式锁后端会开启一个过期的探测,用户可以往里面注册回调,当lock过期的会触发回调,这样可以让用户去觉得如何才能中断你的处理逻辑;
- 上面的这种探测会因为锁是否开启自动续约会有不同的机制;
release lock这个主要问题在于
unlock会不会释放别人的锁,这个比较尴尬;解决的方式还是在于lock是有ownership的,你unlock的时候需要对ownership进行验证,这样的话如果不是你的锁或者你的锁过期了的话,解锁过程也是对系统不会造成影响的;
3. 目前没有涉及的问题
网上有很多的问题其实并不是关于分布式锁如何实现,而且关于分布式锁依赖的后端如果没有高可用怎么办;最典型的就是redis了,不过这次我不讨论这个,等我研究一下中间可能存在的问题和解决思路再来写一篇总结;目前我选的是mongo,mongo支持多副本高可用强一致性的,所以用类似的就没这些问题;