面试中如何回答分布式锁实现方案才算合格

Posted 洞悉源码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试中如何回答分布式锁实现方案才算合格相关的知识,希望对你有一定的参考价值。

面试中如何回答分布式锁实现

最近在找工作,然后看很多文章,有些文章写得非常好因为他在不断地引导你去独立地思考,而有的文章只是给出你一个最终的解决方案与答案,然而背后为什么要这么做去确没有给出解析与分析。后面这类文章可能看啦,记住啦就能应对一般的面试,但真正牛的面试官更多的是考查面试者的独立思考能力与知识迁移的能力。如果你只是看了很多文章而没有去思考与总结,碰到这类面试官基本没戏,并且也很易容忘记。今天来说说面试中经常被问到的一个问题,如何实现一个分布式锁。常见的两种实现方式如下
1、利用redis实现
2、利用zookeeper实现

最常见的redis实现分布式锁方案

可能大家都记得它的实现如下:

加锁
//加锁命令
set resoure_name unique_value px 20000 nx
解锁lua脚本,release.lua
//解锁lua脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
解锁
//1代表KEYS中只有一个元素resoure_name,unique_value为ARGV中元素
eval release.lua 1 resoure_name unique_value

加锁时在client端执行上面的加锁命令就可以,如果set命令返回OK则代表加锁成功,否则加锁失败;解锁时在client上执行对应的lua脚本(release.lua)便可,解锁成功返回非0, 否则返回0。lua中table(可以理解为数组)的下标是从1开始的。
现在问题来啦,在加锁时为什么要做下面几件事情呢?
1、set resoure_name (要获取的获取)时指为什么要指定nx选项?不指定可以吗?不指定会有什么问题?
2、set resoure_name (要获取的获取)时指为什么要指定px选项?不指定可以吗?不指定会有什么问题?
3、set resoure_name (要获取的获取)时指定的unique_value可以去除吗?
4、上面实现的分布式锁,在配置了主从或是集群的情况下,这种方式实现的分布式锁会有问题吗?
加锁时问题1解答:
set resoure_name指定nx选项是为了用原子的方式判断对应resoure_name是否已被设定,如果没有设置才有机会设置成功。不指定nx选项有可能导致多个client在同一时候获得同一个资源,加锁这个动作的互斥性被破坏。
加锁时问题2解答:
set resource_name指定px 20000选项的目的是,防止一个client在获取到锁后便宕机,这时锁将永远无法被释放掉,导致其他的client无法获取到锁产生饿死的现象。
加锁时问题3解答:
set resoure_name时指定的unique_value不可以去除。考虑一种场景当client1获取到锁后,由于某些原因被阻塞且阻塞的时间很长,达到了px选项指定的自动过期时间,此时clinet2刚好来获取同一把锁,由于之前clientl加的锁已自动过期,此时client2能加锁成功,后面client1从阻塞中唤醒,并执任务后调用的解锁操作,即将服务端的key给删除,实际上的效果是把client2加锁给解锁啦。所以在加锁时要指定一个全局唯一的unique_value作为value,防止一个client,将另一个client加的锁给删。可能些人会觉得这个场景本身就不合理,因为client1的任务没执行完锁便释放,当client1从阻塞状态唤醒后,再次执行任务,此时相当于client1与client2都获得了锁,分别再执行任务,原本通过分布式锁保护的共享资源便不受保护,程序执行的结果便得不可预期。
加锁时问题4解答:
上面给出的redis分布式锁实现方案只适合非主从,非集群架构下面的redis,在主从或群集架构下面的redis如果采用上面的方案有可能会出现已加的锁丢失的情况,最终原本通过分布式锁保护的共享资源便不受保护。从主架构下面,考虑如下场景,client1通过加锁操作成功地从master节点上获取锁,然后master节点宕机,但resource_name对应的数据还没有被复制到slaver节点上,sential发现master节点不可用,将slaver节点提升为master节点;client1还在执行其任务没有释放锁,client2请求当前的master节点获取resource_name对应的锁,由于之前client1加锁的数据resource_name没被复制过来,client2加锁也成功,此时相当于client1与client2都获得了锁,分别再执行任务,原本通过分布式锁保护的共享资源便不受保护,程序执行的结果便得不可预期。
集群架构下面也会出现似类的情况,不再分析。
最后一个问题解锁时,为什么要用lua脚本,而不能直接执行对应的命令?
这个相对简单,redis下面执行lua脚本能将多个操作变成一个原子操作。底层是怎么将多个操作变成原子操作的表示暂时还没看,后面有机会再看。

如何利用redis实现非单点的分布式锁服务

上面讨论的用redis实现的分布式锁只适合单节点且不是主从架构时,那有没有更好的方案能消除单点使得最终实现的分布式锁的可用性更高呢?答案是肯定的,redis的作者提出了一种叫作RedLock的实现方式。
实现原理如下,假设有N(N通常为奇数)redis节点,这些节点都是相互独立的并且没有配置slaver节点,也没有通过任务隐式的系统进行协调。每个redis节点上,获取与释放锁的方式和之前单节点获取与释放锁的方式一致。一般情况下取5个redis节点就可以。客户端将按照如下步骤获取分布式锁:
1、客户端获取当前系统的毫秒时间t1。
2、客户端依次向锁服务依赖的N个redis节点按照相同的resource_name与unique_value发送获取锁的请求。具体的命令与上面讲的单节点实现基本一致。只是客户端在向每个实例请求获取锁时会设置一个超时间,这个超时间远小于分布式锁定自动释放时间。例如,如果自动释放时间是10秒,则超时可以在5-50毫秒范围内。这样可以避免在redis节点已宕机时,客户端试还等待响应结果。如果redis节点不可用,应该尝试尽快请求另外一个redis节点。
3、客户端获取锁的时间为t2-t1,其中t2为再次客户端获取的当前系统的毫秒时间。当且仅当客户碳能够在大多数实例中获取锁(N/2+1),且获取锁定所经过的时间t2-t1小于锁定有效时间t3,客户端获才真正获取分布式锁。
4、如果客户端成功获取分布式锁,其真正的有效时间为t3-(t2-t1)。
5、如果客户端由于某种原因(无法在大多数reids节点上,即N/2+1的reids节点上获取锁或者是在步骤4中计算出来的真正有效时间为负)无法获取锁,客户端将尝试释放之前所有redis节点上加的锁(即使它认为不是能够锁定)。
上面的reids作者分布锁的实现方案有没有问题呢?
分布式专家Martin Kleppmann提出了质疑,而redis作者也做出的回应。这个有点深入啦,本人暂时没有细看,感兴趣的同学可以看看原文,或者是。在redis官网上有更详细的关于RedLock的介绍与分析,参考中有链接。

利用zookeeper实现分布式锁服务

利用zookeeper实现分布式锁服务,获取锁的步骤如下:
1、客户端client1创建临时顺序节点/resource_name,假设返回结果为nodeid1。
2、客户端client1获取/resource_name下所有孩子节点,用步骤1中的nodeid1的序号与所有子节点的序号比较,看看自己的序号是不是最小的。如果是则获取分布式锁成功。如果不是最小的则wach比该节点返序号nodeid1次小的节点的事件,假设比nodeid1序号次小的序号为nodeid0,并且由客户端client0创建,然后挂起当前线程。
3、当创建比序号nodeid1次小的序号nodeid0客户端client0,处理完业务逻辑时,会删除其对应的节点nodeid0。client1监听到该事件后,则再次执行步骤2。
现在问题来啦?
1、为什么要采用临时顺序节点实现?
2、如果现在步骤2中,创建返回序号nodeid1的节点对应的client1 watch的创建返回序号nodeid0的节点对应的client0宕机啦?此时client1还能获取锁吗?
问题1解答:
临时顺序节点是为了避免客户端在创建完结点后就宕机导致其它客户无法获取锁而产生饿死。采用临时顺序节点,利用了临时节点的特性,当客户端断开链接时节点会自己被删除。而节点顺序性为了,一方面是为了保证同一时刻只有一个客户端加锁成功,即创建节点返回序号最小那个客户端,另一方面是为在锁释放后达到通知比其大一号节点获取锁的目的。这个和JDK并发包中ReentranceLook的实现锁的原理有着异曲同工的地方,ReentranceLook主要靠Sync协同类实现加锁与释放锁。Sync实现了AQS类。AQS中维护着一个双向链接构成的同步队列,当一个线程尝试获取锁失败时,该线程会被包装成一个节点加入队列的尾部,通过LockSupport.park方法挂起处于阻塞状态;当之前成功获取锁的线程释放锁时会调用AQS的release方法将将同步队列尾部处于挂起的线程给唤醒,实际是通过LockSupport.unpark实现。此时被唤醒的节点会看其对应的前驱前点是不是头点,如果是的话再次尝试获取锁。采用同样的原理,我们可以利用zoookeeper实现分布式锁,并且让其具有重入性、公平性、非公平性等特征。如果有时间后面会尝试写一个。
问题2解答:如果client0宕机,那个其它对应的临时顺序节点将被自己动删除,这时client1能够监听到这个事件,继续执行获取分布式锁中的步骤2。

总结

这个文章写的有点多,主要是介绍了分布式锁实现的两种试,分别是用redis实现与利用zookeeper实现方案。在介绍redis方案后,分析了其原理与可能出现的问题,然后由于引出了RedLook。对于RedLook部分本文没有深入讨论,只是为在这个抛砖引玉。然后又介绍了zookeeper实现分布式锁的方案,并简单分析了其原理,并顺带简单介绍了JDK并发包中ReentranceLook如何使用AQS实现锁。主要的目的是为了知识的迁移,如果是面试官的就会从zookeeper实现分布式锁的原理这个问题,迁移到JDK并发包ReentranceLook的锁是如何实现。进而再进一步问,ReentranceLook的重入性、公平性、非公平性是怎么实现,然后再迁移回,如何用zookeeper实现分布式锁,并且实现重入性、公平性、非公平性等特性。可惜本人不但不是面试官,还在失业中。。。写得不对的地方大家拍砖过来吧哈哈

参考:

https://redis.io/topics/distlock
http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
http://antirez.com/news/101
https://mp.weixin.qq.com/s/goZDSKaisVJRL_OCL5gOnQ
https://mp.weixin.qq.com/s/Uya33qfxO0Xy3B76GmAHZQ

以上是关于面试中如何回答分布式锁实现方案才算合格的主要内容,如果未能解决你的问题,请参考以下文章

Java进阶之光!2021必看-Java高级面试题总结

Java工程师面试题,二级java刷题软件

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

如何实现分布式锁?源码+原理+手写框架

Http知道这些,开发Android才算合格!

分布式锁三种解决方案