分布式锁的实现方式有很多中,比较常用的有redis,zookeeper,数据库等,本文将介绍下常用的实现方案以及优缺点,首先分布式锁也和其他锁一样,有共享锁,有排他锁,共享锁的比较简单,本文将主要讨论排他锁的获取锁和释放锁过程
redis
redis实现分布式锁也有很多方法,我们可以自己实现,也可以直接使用现有的基于redis的组件,比如redission
redis命令实现
目前主流的使用reids实现分布式锁的方案中,以下是比较适合大部分业务场景的,即使用如下命令实现
1 | 加锁 |
1 | 释放锁 |
以上分别为获取锁和释放锁的命令,针对以上命令,我们可能会有如下疑问
- 为什么使用nx命令
nx的语意是只有在key不存在的情况下,才会设置成功,这个语意再加上上述set命令是原子操作,这样就确保了在同一时刻,只有一个请求能够设置成功key值,也就是只有一个请求能拿到锁 - 为什么使用EX或者PX
EX或者PX的语意都是给key设置一个过期时间,我们知道,即然有了获取锁的过程,那么必然会有释放锁的过程,对于上述方案而言,释放锁其实就是删除key,那么加入程序在删除key的时候出问题了,即使是finally也没有执行到,那么就会造成锁释放不了的问题
所以,我们要给key加一个过期时间,避免锁无法释放的问题 - key的值为什么要随机
加入A和B同时要获取锁,结果A获得,B没获得,那么在A获得之后,会给key一个过期时间,比如是1s,但是A业务逻辑执行了2s,才开始释放锁,那么在多于执行的这一秒之类,加入B在尝试获取锁,是可以获取成功的,接下来A再去释放锁,也就是删除key,这就意味着可能会释放其他用户锁获取的锁。
所以,我们在获取锁的时候,要给key指定一个随机值,这样在释放的时候将预先设施的随机值和redis里面的value比较,如果相同,那么就说明我们释放的是同一个用户获取的锁 - 释放锁为什么要用lua脚本
上述第三点也解释到了,我们释放锁的过程要分为两部,获取key的值,然后和预先设置的随机值比较,那么这就不是原子操作了,将会带来新的问题,比如get完之后刚好key的过期时间到了,也就意味着if判断通过了,开始执行删除key操作了,那么这中间B可能获取到锁了,这样,A又会把B的锁释放了。
综上,我们需要释放锁为原子操作,所以选择lua脚本
以上命令中,ARGV[1]表示设置key时指定的随机值。KEYS[1]表示需要加锁的资源,针对以上命令,我们可能会有如下疑问
redisson
1 | 加锁代码 |
以上代码为redisson的获取锁逻辑,为了原子性,也是lua实现,相比于我们set nx的方式,redisson有以下特点
- 使用watch dog实现自动延期,比如A获取到锁,并且设置过期时间20s,但是业务执行了30s,那么watch dog线程就会检查到A还持有锁,然后就会延长key的存活时间
- 可重入锁,在上述代码第二个判断加锁参数一致的情况下,value+1,实现可重入
- 释放锁的过程中,每次对value-1,如果为0,则说明锁释放,执行del命令
总结:在生产环境中,建议使用redisson,redisson的设计更加全面,而且提供了丰富的锁类型,比如公平锁,非公平锁,读写锁,闭锁,信号量等
不管是自己实现还是使用redisson,都会面临一个问题,那就是master-slave发生主备切换的时候,可能会发生A和B都获取到锁的情况,比如A在master上获取到锁了,这个时候master挂了,slave变为master,B再去获取锁的时候,是可以在新的master上获取到的
zookeeper
和redis实现分布式锁一样,zookeeper我们也可以自己使用zookeeper实现分布式锁,也可以直接使用现有的一些开源组件,比如cuartor,cuartor是zk的客户端实现,curator之于zk就像guava之于java。
由于自己代码实现比较复杂,且cuartor已经做了很好的封装,提供了完善可靠的锁机制,接下来我们将简单分析下cuartor基于ZooKeeper实现分布式锁的原理
- 创建一个目录mylock;
- A想获取锁就在mylock目录下创建一个临时顺序节点
- 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- 这时候B想获取锁,他会做同样的事情,在mylock目录下建一个零时顺序节点,并判断自己不是最小节点,如果是,获得锁,如果不是,获取锁失败,然后设置监听比自己次小的节点
- A处理完,删除自己的节点,释放锁,B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
针对以上加锁流程,我们可能会产如下疑问
- 为什么是临时节点
临时节点是为了在客户端出现宕机,且没有主动释放锁的时候,zk能够感知到客户端的异常,并且删除对应的节点,也就是释放锁 - 为什么是顺序节点
节点的顺序是为了锁的阻塞特性,公平特性
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
数据库
基于数据库的实现方式比较简单粗暴,主要使用数据库的唯一索引来实现,即A和B同时插入同一条数据,由于唯一索引的限制,只能有一条插入成功,插入成功即拿到锁
- 锁开销比较大,需要操作数据库
- 阻塞策略需要自己实现,在B插入失败之后,如需阻塞,需要实现重试等逻辑
- 强依赖数据库,数据库的可用性和性能将直接影响分布式锁的可用性及性能