在Java面试中应对分布式锁问题的高招

发表于:2020-7-07 09:04

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:JAVA资料那些事呀    来源:SegmentFault博客

#
java
分享:
  面试官 :项目中使用过分布式锁吗?
  小小白:用过。
  面试官:为什么要使用分布式锁?
  小小白:为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用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方式。

本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号