分布式锁实现选型🤔

577 阅读5分钟

分布式锁

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

我们知道在同一个JVM中,可以通过Volatile、Synchronized、ReentrantLock 三个关键字来实现线程的安全。

那么在分布式系统中这些是无法保证的,所以要通过分布式锁来实现。

基于分布式锁的实现有多种方案,现针对Redis 和Zookeeper 这两种方式聊一聊功能具体实现方式、优缺点以及各自适用的业务场景。

基于Redis Cluster模式下的分布式锁的实现

基于setnx 锁实现

加锁

正常基于Redis 自带的setnx 命令就可以实现简单的加锁功能:

> setnx key value

Key 作为锁的唯一标识,当线程setnx返回1时,说明原本key不存在,该线程成功得到了锁;如果返回的结果为0,则说明key已经存在,线程获取锁失败。

解锁

当线程获取锁,执行完任务后,需要释放锁,以便后续线程使用。可以通过del 这个key来实现:

> del key

但是由于加锁和解锁是分为两步实现,不是原子操作,所以可能会出现中间状态:即加锁完成后,没有解锁(或解锁失败);导致资源锁住。

基于此有这种方案:

通过制定key的过期时间,让锁到期后自动释放:

> expire key

显而易见,这边也有一个上述的风险点:就是非原子操作,可能存在中间状态。

所以我们引入第二种方案。

基于set 原子操作实现:

> set key value [EX seconds] [PX milliseconds] [NX|XX] EX seconds:设置键key的过期时间,单位时秒;

PX milliseconds:设置键key的过期时间,单位时毫秒;

NX:只有键key不存在的时候才会设置key的值;

XX:只有键key存在的时候才会设置key的值。

通过 set (key, value, EX a,NX) 取代setnx 来实现原子加锁(自动释放锁)操作。

基于Redis 实现分布式锁的一些问题 在高并发下的分布式锁实现中,key的过期肯定不能设置的太长,否则会影响后续线程持有该锁;

但是如果设置过期时间很短,直到key过期,持有该锁的线程还未执行完任务;接着下一个线程获取到该锁,这时候前一个线程执行完成后触发del释放该锁,而这把锁这个时候其实是另外一个线程持有;

获取锁是非阻塞的,无论成功还是失败就直接返回;

锁公平问题,所有等待线程同时发起获取锁命令操作。

针对上述问题需要另外服务来保证实现:

  • 守护线程:如果某个线程在expire时间内,还未执行完成,守护线程自动expire一个新过期时间,直到该线程执行完成或释放;

  • 释放验证:线程加锁前通过线程ID和Key Value匹配,释放前通过两者判断是否一致,一致再释放该锁,避免错误释放其他线程执行时持有相同的锁(原子性可以通过lua脚本来实现);

  • 阻塞锁: 通过while true之类的机制去阻塞代码实现;

  • 公平锁:可以通过将所有等待线程放入同一个队列来实现。

可重入性

Java中的Lock对象以及Synchronized关键字语块都可具有可重入性,可以实现同一个线程中共用同一把锁;避免死锁发生的可能。

而在Redis上述实现中则没有相应的功能,如果业务上需要,则需在业务代码中实现其逻辑。

安全性

Redis Cluster 在master异常情况下,会发生主从切换,而主从是异步复制,极大可能导致数据丢失,从而导致锁的失效。这块安全性方面Redis Cluster 无法保证。

但是Redis 作者实现了基于多节点的高可用分布式锁的算法 RedLock。www.redis.cn/topics/dist… 有兴趣的可以了解一下。

基于Zookeeper 集群模式下的分布锁的实现

Zookeeper 通过临时有序(顺序)节点实现分布式锁。

所谓临时顺序节点就是Zookeeper根据创建的时间给该节点名称进行编号,当创建节点的客户端与Zookeeper 断开连接后,临时节点就被删除。

Zookeeper 通过创建临时有序节点实现上锁,只有序号最小(或顺序最靠前)的可以成功获取到锁;如果该序号不是最小(或不是顺序最靠前)则向它前一个节点注册Watcher,通过watch来监听前一个节点是否存在,等待watch事件(即监听节点的状态变化),如果监听到watch事件发生,则再次判断该节点是否为序号最小节点,如果是则成功获取锁,否则,继续监听等待。

Zookeeper 实现分布式锁的功能

Zookeeper 实现锁的方式上较为简单。

  • 由于Zookeeper 使用集群模式,可以避免单点故障;

  • 临时有序节点,临时节点可以避免锁的中间状态(即永久持有该锁);有序可以保障锁的公平问题;

  • 通过watch实现阻塞。

Zookeeper 实现分布式锁的一些问题

每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

所以性能开销这块比Redis 大,即并发性能不如Redis。

Redis 和 Zookeeper 分布锁实现比较

实现方式优点缺点适用场景
Redis Cluster实现指令性能较高实现较为复杂;需要额外多个服务来保障;安全性较低。高并发的分布式锁实现
Zookeeper现有的框架,实现简单;等待锁队列,提升抢占锁效率。添加和删除节点性能较低并发量小,安全性要求较高的业务场景