共计 2784 个字符,预计需要花费 7 分钟才能阅读完成。
文末有面试资料福利!
面试官:项目中使用过分布式锁吗?
小小白:用过。
面试官:为什么要使用分布式锁?
小小白 :为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用 Java 并发处理相关的 API(如 ReentrantLcok 或 synchronized) 进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
面试官:项目中用到的分布式锁是你自己实现的,还是别人写的?
小小白:公司中间件部门二次开发的。
面试官:有没有研究过它的具体实现?
小小白:它是在 Redisson 的基础上再次封装的,因为 Redisson 已经实现了一套完整的分布式锁解决方案,所以只要做简单的封装就是可以很轻松的使用。
面试官:有没有了解过 Redisson 实现的分布式锁原理?
小小白 :使用 key 来作为是否上锁的标志,当通过 getLock(String key) 方法获得相应的锁之后,这个 key 即作为一个锁存储到 Redis 集群中,在接下来如果有其他的线程尝试获取名为 key 的锁时,便会向集群中进行查询,如果能够查到这个锁并发现相应的 value 的值不为 0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞,否则由当前线程获取这个锁并将 value 值加一,如果是可重入锁的话,则当前线程每获得一个自身线程的锁,就将 value 的值加一,而每释放一个锁则将 value 值减一,直到减至 0,完全释放这个锁。底层通过 eval 命令来执行 Lua 脚本,保证复杂业务逻辑执行的原子性。
面试官:如果让你实现一个分布式锁,你会有哪些实现方案?
小小白:这个之前有了解过,基于数据库的实现方式、基于 Redis 的实现方式和基于 ZooKeeper 的实现方式。
面试官:使用数据库如何实现?
小小白:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
面试官:据我了解这种实现方案基本没人使用,为什么?
小小白:这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决:
- 因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
- 不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
- 没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
- 不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
面试官:使用 Redis 如何实现分布式锁?
小小白 :在 Redis2.6.12 版本之前,使用 setnx 命令设置 key-value、使用 expire 命令设置 key 的过期时间获取分布式锁,使用 del 命令释放分布式锁,但这种实现方式会出现死锁、误删持有的锁、主从机制数据不同步的问题。所以,从 Redis2.6.12 版本开始,通过 SET resource_name my_random_value NX PX max-lock-time 来实现分布式锁,这个命令仅在不存在 key(resource_name) 的时候才能被执行成功(NX 选项),并且这个 key 有一个 max-lock-time 秒的自动失效时间(PX 属性)。这个 key 的值是“my_random_value”,它是一个随机值,这个值在所有的机器中必须是唯一的,用于安全释放锁。同时,释放锁的时候,只有 key 存在并且存储的“my_random_value”值和指定的值一样才执行 del 命令,此过程通过 Lua 脚本执行,保证原子性。而且,不采用主从复制机制,使用 RedLock 算法解决获取锁和释放锁的单点故障问题。
面试官:你刚刚说到 RedLock 算法,它的原理是什么?
小小白:在 Redis 的分布式环境中,假设有 5 个 Redis master,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端执行以下操作:
- 获取当前 Unix 时间,以毫秒为单位;
- 依次尝试从 N 个实例,使用相同的 key 和随机值获取锁。在步骤 2,当向 Redis 设置锁时, 客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5 -50 毫秒之间。这样可以避免服务器端 Redis 已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个 Redis 实例;
- 客户端使用当前时间减去开始获取锁时间(步骤 1 记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是 3 个节点)的 Redis 节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果);
- 如果因为某些原因,获取锁失败(没有在至少 N /2+ 1 个 Redis 实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功)。
面试官:你再说一下基于 ZooKeeper 的实现方式?
小小白:基于 ZooKeeper 实现分布式锁的步骤如下:
- 创建一个目录 mylock;
- 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;
- 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
- 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
推荐使用 apache 的开源库 Curator,它是一个 ZooKeeper 客户端,Curator 提供的 InterProcessMutex 是分布式锁的实现,acquire 方法用于获取锁,release 方法用于释放锁。
面试官:基于 ZooKeeper 的实现方式有什么优缺点?
小小白:高可用、可重入、阻塞锁特性,可解决失效死锁问题,但是因为需要频繁的创建和删除节点,性能上不如 Redis 方式。