2023年(Q3财年)技术部门CTO线技术人员晋升考核机试题

分布式篇-A 分布式锁

*参考答案*

出题人:湖北TL田超凡
答案制定:湖北TL田超凡

*****试卷启用前绝密****

1 什么是锁?什么是分布式锁?二者有什么区别?

答:

  1. 锁是线程级别的概念,锁的目的是保证线程间业务代码执行的幂等性,同一时刻只有一个线程能够获取到锁并执行业务代码,其他没获取到锁的线程会一直阻塞等待获取到锁的线程释放锁之后才会被唤醒并重新进入到竞争锁的状态。
  2. 分布式锁是JVM级别的概念,分布式锁的目的是保证分布式或微服务集群环境下业务代码执行的幂等性。他的设计思想是多个JVM同时去执行相同的业务代码(比如定时任务),保证同一时刻只有一个JVM能够获取到分布式锁并得到执行机会,其他未获取到锁的JVM会一直处于阻塞状态,直到获取到锁的JVM释放锁之后才会被唤醒并重新进入到竞争分布式锁的状态。

2 synchronized锁和分布式锁的区别?

答:

  1. Synchronized锁是线程级别的锁,是悲观锁。它的作用是可以保证在同一个JVM中多线程环境下同一时刻只有一个线程能够获取到锁,其他未获取到锁的线程都会被阻塞等待,直到获取到锁的线程释放锁之后,被阻塞等待的线程才会被唤醒并重新进入到获取锁的状态。
  2. 分布式锁是JVM层面的锁,用来在服务集群环境下多个JVM同时获取锁的情况下,同一时刻只有一个JVM能获取到锁,其他未获取到锁的JVM都会被阻塞等待,直到获取到锁的JVM释放锁之后,才会被批量唤醒并重新得到竞争锁的机会。

3 分布式锁的应用场景并举例分析?

答:

应用场景:分布式服务集群环境下定时任务执行的幂等性问题

举例分析:假设现在有两个服务,一个是服务A,一个是服务B,我们都知道每一个服务本质上都是一个单独的JVM,假设现在这两个服务里面都有一段相同的定时任务代码,需要每天定时只是执行一次,但是两个JVM触发CRON表达式的时候都会去执行相同的定时任务代码,这样很明显就违背了业务需要,破坏了幂等性,这种场景下就可以使用分布式锁来保证定时任务执行的幂等性问题(每一天只会执行一次定时任务,不会所有的服务都去执行)。

4 分布式锁的实现方式并简述?

答:

目前主流的分布式锁的实现方式有两大种:

一种是基于Zookeeper实现分布式锁,一种是基于Redis实现分布式锁。

但是他们的设计思想都是类似的:最终都要实现相同的目的来保证业务代码执行的幂等性问题。

分布式锁的实现本质是,当多个JVM同一时刻去获取分布式锁的时候,保证只有一个JVM能获取到分布式锁,其他未获取到分布式锁的JVM都会被阻塞等待,知道获取到锁的JVM释放锁之后,才会被批量唤醒并重新进入到竞争分布式锁的状态。

5 Zookeeper实现分布式锁和Redis实现分布式锁的区别?

答:

  1. Zookeeper实现分布式锁:数据同步采用同步的方式实现,优点是可以保证各个节点数据一致性;缺点是写操作效率低,可能会出现长时间阻塞超时的问题,满足一致性,不满足可用性,是典型的CP模式实现,并且具有过半机制可以先天性解决集群脑裂的问题。
  2. Redis实现分布式锁:数据同步采用异步的方式实现,优点是写操作效率高,性能优越;缺点是无法保证较强的数据一致性,满足可用性,不满足一致性,是典型的AP模式实现,没有过半机制可能会存在集群脑裂的问题。

6 Zookeeper实现分布式锁的步骤和实现原理?

答:Zookeeper实现分布式锁的核心实现原理是基于临时节点+事件通知实现。Zookeeper临时节点路径是唯一的,不能重复。

Zookeeper实现分布式的步骤主要包括三个阶段:获取锁、释放锁、被唤醒。

获取锁:该阶段多个JVM会同时在zookeeper创建相同路径的临时节点,由于zookeeper临时节点路径是唯一的,不能重复,所以同一时刻多个JVM创建相同路径的临时节点,只有一个JVM能创建临时节点成功,也就意味着该JVM获取分布式锁成功;其他创建临时节点失败的JVM就会自动重试,当超过一定的重试次数后会阻塞等待,同时基于事件监听机制监听获取到锁的JVM创建的临时节点。

释放锁:本质上是删除分布式锁对应的zookeeper临时节点,当获取到锁的JVM释放锁之后,其他没获取到锁的JVM就会监听到该临时节点被删除,此时就会被唤醒并重新进入到竞争分布式锁的状态。

被唤醒:没获取到锁的JVM会基于zookeeper事件监听机制监听获取到锁的JVM创建的临时节点是否被删除,当监听到该临时节点被删除,这些阻塞状态下的JVM就会直接被唤醒并重新进入到竞争分布式锁的状态。

7 Zookeeper实现分布式锁的两种实现方案?

答:

  1. 基于临时节点实现

实现原理:由于Zookeeper临时节点路径都是唯一不可重复的,所以多个JVM尝试同一时刻创建相同路径的临时节点只会有一个JVM创建成功,则创建成功的JVM我们就认为他获取到了分布式锁。但是这样做会有羊群效应的问题。

  1. 基于临时顺序编号节点实现

实现原理:专门用来解决羊群效应问题的解决方案,是zookeeper实现分布式锁目前主流的实现方案,实现原理是多个JVM创建相同路径的临时节点时,每个JVM会在创建的临时节点路径的后面生成一个全局唯一的数字编号,谁创建的临时节点数字编号最小,谁就获取到了分布式锁。获取到分布式锁的JVM会被创建的临时节点路径编号比它大1的JVM订阅,当获取到分布式锁的JVM释放锁之后,只会唤醒订阅了他的JVM重新进入到竞争锁的状态,而不是唤醒全部没获取到分布式锁的JVM,提高了唤醒效率,减轻了Zookeeper 服务器端的唤醒压力,避免出现阻塞的问题。

8 Zookeeper实现分布式锁的超时定义以及怎么解决?

答:

超时是指获取到分布式锁的JVM长时间不释放锁导致释放锁超时。

解决方案:此时可以采用续命设计实现,定时任务重试判断业务逻辑代码是否执行完毕,如果执行完毕就直接释放锁,如果续命多次还没有释放锁则需要手动实现以下策略,避免出现死锁问题:

  1. 主动释放锁(删除该锁对应的zookeeper临时节点)
  2. 事务回滚
  3. 终止当前业务线程
  4. 移除监听

9 Zookeeper实现分布式锁的续命设计怎么实现?

答:可以基于定时任务实现,每隔一段时间检查一下获取到分布式锁的JVM中的业务代码是否执行完毕,如果没有执行完毕则重试几次,如果重试几次还是没有执行完毕,就可以手动释放该JVM占有的分布式锁,避免出现死锁问题。

10 什么是羊群效应?如何避免羊群效应?

答:羊群效应是zookeeper实现分布式锁采用临时节点实现出现的问题。

当竞争分布式锁的JVM数量非常多的情况下,同一时刻只有一个JVM能创建临时节点成功(获取锁成功),其他未获取到锁的JVM全部都会被阻塞等待。当获取到锁的JVM释放锁之后,会全部唤醒这些大量正处于阻塞状态的JVM,导致唤醒的成本非常高,Zookeeper服务器端唤醒效率低下。

避免羊群效应:基于临时顺序编号节点实现。

11 什么是死锁?如何避免死锁?

答:死锁是指未获取到锁的JVM长时间处于阻塞等待状态,无法被唤醒。

避免死锁的方式:

(1)基于zookeeper实现分布式锁:对获取到锁的JVM采取重试机制,如果重试多次或超过一定时间还没有释放锁,则强制主动释放锁(删除zookeeper中该锁对应的临时节点)。

(2)基于redis实现分布式锁:设置key的expire过期时间,过期之后直接删除该key

12 Zookeeper集群选举的实现原理?

答:zookeeper集群选举过程中,会导致zookeeper集群短暂不可用,此时zookeeper也无法实现分布式锁(无法创建临时节点),这样做的目的是保证不同zookeeper节点的数据一致性,所以说zookeeper对于节点的数据一致性控制是非常严格的。

  1. 先比较每个zk节点的zxid,zxid谁最大谁就是为leader节点
  2. 若zxid相同,则比较zxid相同的节点的myid,myid谁最大谁就是为leader节点。

13 Zookeeper主节点宕机,怎么避免死锁?

答:zookeeper leader节点宕机后,zookeeper客户端会接收到zookeeper服务器端发出的断开长连接监听,监听到之后主动释放锁。

14 Zookeeper客户端宕机,怎么避免死锁?

答:zookeeper先天性解决了该问题,当zookeeper客户端宕机后,zookeeper服务器端会长时间接收不到zookeeper客户端发出的续约申请,此时zookeeper服务器端就会默认该zookeeper客户端宕机,直接从集群中移除该节点。

15 Redis实现分布式锁的两种实现方案?

答:

  1. 基于setnx命令实现
  2. 基于Redission框架实现

16 setnx命令和set命令的区别?

答:

  1. Setnx命令:需要创建的key在redis如果不存在,则创建key,并返回设置成功。如果已存在,则创建失败,直接返回设置失败。
  2. Set命令:需要创建的key在redis如果不存在,则创建key。如果已存在,则直接覆盖。

17 Redis实现分布式锁如何避免死锁?

答:可以通过设置key的expire超时时间来避免死锁。

18 Redis实现分布式锁如何保证获取锁和释放锁是同一个线程?

答:setnx设置锁的时候,在value后面加一个UUID加以区分,获取锁的时候生成UUID并记录,当释放锁的时候判断获取到的value的UUID和获取锁的UUID是否相同,如果相同则说明获取锁和释放锁是同一个线程,可以直接释放。

19 Redis实现分布式锁业务逻辑执行时间超过锁过期时间怎么处理?

答:可以采用续命设计,基于watch dog看门狗线程实现,实现原理是:当获取到锁的JVM成功获取到锁之后,立即创建一个续命线程,基于定时任务去检测获取到锁的JVM业务代码是否执行完毕,如果没有执行完毕则延长key超时时间实现续命。但是这样不断续命也可能会产生死锁问题,因为不断续命会导致获取到锁的JVM一直不释放锁,那么其他阻塞状态的JVM就会无法被唤醒,从而出现死锁问题。

所以应该设置一个续命次数,当超过续命次数后,该获取到分布式锁的JVM还没有释放锁,则直接手动释放锁、回滚事务、终止当前线程、移除监听。

20 Redission实现分布式锁的实现原理?

答:

  1. 基于Lua脚本在redis中设置锁
  2. 基于redis hash数据类型存放设置锁的key-value
  3. 内部已经封装了续命设计实现(看门狗线程)的API,按需引用即可。

21 RedLock红锁用来解决什么问题?

答:解决Redis集群环境下主节点宕机后引发的哨兵选举导致分布式锁暂时不可用的问题。

22 简述RedLock的实现原理?

答:

设计思想:

RedLock红锁的设计思想是默认所有redis节点都是一样的,没有主从之分。

RedLock实现分布式锁至少需要保证Redis集群中有3台以上的节点是可用状态。

实现原理:

  1. 客户端对集群中所有redis实例设置锁
  2. 客户端需要设置超时时间,当连接到redis实例设置锁超时,立即切换到其他可用的redis实例设置锁,避免出现阻塞问题。
  3. 客户端需要计算总耗时,当且仅当客户端在Redis集群一半以上的节点设置锁成功,且总耗时小于锁超时时间,才能获取锁成功。
  4. 客户端获取锁失败,所有节点都需要释放掉锁。

23 Curator实现分布式锁的实现原理?

答:本质上是基于zookeeper实现的分布式锁,底层封装了zookeeper常用的API,它的实现原理是:

  1. 首先检查缓存中是否已经存在分布式锁,如果已经存在会直接复用,而不会再次创建。
  2. 如果缓存中没有分布式锁,则创建分布式锁并放入缓存。
  3. 基于临时顺序编号节点实现,避免出现羊群效应问题。
07-05 09:09