一位6年工作经验的小伙伴,在某厂面试时被问到“实现分布式锁,Zookeeper 和 Redis 哪种更好?“,这其实是一个开放性的问题。并没有标准答案。那今天呢,我给大家分享一下我的理解,希望能够帮助到大家。另外,我花了很长时间,准备了一份500页的PDF面试资料文档和一份10W字的Java总结面试题和答案,
1、背景介绍
使用分布式锁的目的,是为了保证同一时间只有一个 JVM 进程可以对共享资源进行操作。
ENTER TITLE
根据锁的用途可以分为以下两类:
1、允许多个客户端操作共享资源,我们称为共享锁。
这种锁的一般是对共享资源具有幂等性操作的场景,主要是为了避免重复操作共享资源频繁加锁带来的性能开销。
2、只允许一个客户端操作共享资源,我们称为排他锁。这种锁一般是用在对共享资源操作具有非幂等性操作的场景,也就是需要保证在同一时刻只有一个进程或者线程能够访问这个共享资源。
2、实现方案
目前实现分布式锁最常用的中间件是 Redis 和 Zookeeper:
首先来看Redis的实现方案,它可以通过两种方式来实现
1. 利用 Redis 提供的 SET key value NX PX milliseconds 指令,
ENTER TITLE
这个指令是设置一个Key-Value,如果 Key 已经存在,则返回 0,否则返回 1,我们基于这个返回值来判断锁的占用情况从而实现分布式锁。
2. 基于 Redission 客户端来实现分布式锁,Redisson 提供了分布式锁的封装方法,我们只需要调用 api 中的 lock()和 unlock()方法。它帮我们封装锁实现的细节和复杂度。
Redission 所有指令都通过 Lua 脚本执行并支持 Lua 脚本原子性执行, Redission 中有一个 WatchDog 的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔 10 秒帮你把 Key 的超时时间设为 30s,就算一直持有锁也不会出现Key 过期了。“看门狗”的逻辑保证了没有死锁发生。
然后,基于 ZooKeeper 实现分布式锁的落地方案
Zookeeper 实现分布式锁的方法比较多。我推荐使用有序节点来实现,来看这个图,
ENTER TITLE
首先第1步,每个线程或进程在 Zookeeper 上的/lock 目录下创建一个临时有序的节点表示去抢占锁,所有创建的节点会按照先后顺序生成一个带有序编号的节点。
第2步、线程创建节点后,获取/lock 节点下的所有子节点,判断当前线程创建的节点是否是所有的节点的序号最小的。
第3步、如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
第4步、如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听,当前一个被监听的节点释放锁之后,触发回调通知,从而再次去尝试抢占锁。
3、两者对比
接下来,咱们来对比一下这两种方案:
对于 Redis 的分布式锁而言,它也有以下缺点:
ENTER TITLE
1、它获取锁的方式简单粗暴,如果获取不到锁,会不断尝试获取锁,比较消耗性能。
2、Redis 是 AP 模型,在集群模式中由于数据的一致性会导致锁出现问题,即便使用 Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100%的可靠性。
不过在实际开发中使用 Redis 实现分布式锁还是比较常见,而且大部分场情况下不会遇到”极端复杂的场景“,更重要的是 Redis 性能很高,在高并发场景中比较合适。
对于Zookeeper 分布式锁而言:
ENTER TITLE
1、Zookeeper 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
2、如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
如果要在两者之间做选择,就我个人而言的话,比较推崇 ZK 实现的锁,因为对于分布式锁而言,它应该符合 CP 模型,但是 Redis 是 AP 模型,所以在这个点上,Zookeeper 会更加合适。
最后,我把之前分享的资料全部整理成了文字,希望能够以此来提高各位粉丝的通过率。