文末有面试资料福利!
面试官 :项目中使用过分布式锁吗?
面试官:为什么要使用分布式锁?
小小白:为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用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,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。为了取到锁,客户端执行以下操作:
面试官:你再说一下基于ZooKeeper的实现方式?
小小白:基于ZooKeeper实现分布式锁的步骤如下:
推荐使用apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
面试官:基于ZooKeeper的实现方式有什么优缺点?
小小白:高可用、可重入、阻塞锁特性,可解决失效死锁问题,但是因为需要频繁的创建和删除节点,性能上不如Redis方式。