[TOC]
关键字
分布式锁
redis
zookeeper
在单机应用中,我们通常可以用synchronized
、ReentrantLock
等去实现资源的索引,从而解决并发竞争问题。
在实际应用中,我们的服务经常是分布式的,那如何管理多台应用机器对竞争资源的访问呢?那就需要用到 分布式锁
。
分布式锁主要理念是通过第三方独立服务管理和分发需要被锁定的资源数据。一般常见的包括使用redis或者zookeeper来实现分布式锁。
基于redis的分布式锁
我们知道,redis是单线程工作模式。因此可以将分布式应用的并发请求串行化。
加锁: redis是用过setnx
命令实现分布式锁。加锁过程为:
setnx(key, value)
setnx 的含义是set if not exist。当命令执行结果返回1,代表key不存在,并且设置获取锁成功。如果返回结果为0,代表key已存在,线程获取锁失败。
释放锁:释放锁,只需要删除对应的key值,就代表释放了相关锁,代码如下:
del(key)
存在的问题
锁无法释放
上边的加锁和解锁过程非原子操作。也就存在可能:一个线程加锁后,没有释放锁。最终导致该所永远无法释放。为了解决该问题,我们在设置锁之后,可以为该锁设置一个自动过期时间:
expire(key, timeout)
然而上述expire和setnx仍然不是原子操作,也就是可能会存在setnx成功后,expire命令未执行。为了解决这种问题,可以使用:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
//EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
//PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
//NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
//XX :只在键已经存在时,才对键进行设置操作。
这样就解决了刚刚提到的非原子问题。
锁错误释放
实际上上述设置超时时间的方式,还可能引发另一种问题。那就是持有锁的线程由于某些原因,执行比较慢,导致在线程未完成的情况下,锁资源由于超时过期而被自动释放。为了解决这类问题,可以做如下操作:
- 引入守护线程,在线程锁即将过期的时候,重新刷新过期时间,直到线程执行完毕后,主动删除锁并关闭守护线程。
- 释放锁引入线程ID验证:这个方案主要是为了防止某些情况,当前线程的锁超时被释放,然后被另一个线程持有。如果不做线程ID之类的验证,当前线程就可能错误的释放了其他线程持有的锁,导致出现问题。即在释放前先校验key下的value是否为线程ID,如果是再进行释放,两步操作的原子性可通过lua脚本来实现。
基于zookeeper的分布式锁
Zookeeper实现分布式锁主要用到了一种叫做顺序节点。
假如我们在/lock/目录下创建节3个点,ZooKeeper集群会按照提起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003。
ZooKeeper中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZooKeeper集群断开连接,则开节点自动被删除。
实现分布式锁的基本逻辑:
客户端调用create()方法创建名为“locknode/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为临时节点。
-
客户端调用getChildren(“locknode”)方法来获取所有已经创建的子节点。
客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁。
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
参考文献
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
https://zookeeper.apache.org/doc/current/recipes.html#sc_recipes_Locks