分布式锁的三种实现方式面试,分布式锁面试题

  分布式锁的三种实现方式面试,分布式锁面试题

  00-1010简介1。面试官:你有需要使用分布式锁的场景吗?事件A:事件B: 2。面试官:Redis分布式锁1的实现方法。基于Redis 3的分布式锁。面试官:解锁操作呢?用del命令解锁。3.采访者:基于ZooKeeper的分布式锁实现原理。追加补充方法一:方法二:总结。

  00-1010锁是开发过程中非常常见的工具。你一定很熟悉它。悲观锁、乐观锁、排他锁、公平锁、不公平锁等。有很多概念。如果你对java中的锁还一无所知,可以参考这篇文章:你不得不说的Java中的“锁”。这篇文章非常全面,但是对于初学者来说,知道这些锁的概念是由于缺乏实践。Java可以通过Volatile、Synchronized和ReentrantLock实现线程安全。这部分知识在基础面试第一轮肯定会被问到(要熟悉)。

  在分布式系统中,Java锁技术不能同时锁定两台机器上的代码,所以要用分布式锁来实现。熟练使用分布式锁也是大厂商需要培养的必备技能。

  

目录

 

  00-1010问题分析:这个问题主要作为引子。首先,你要知道什么场景需要使用分布式锁,分布式锁要解决什么问题。在这个前提下,会帮助你更好的理解分布式锁的实现原理。

  使用分布式锁的场景通常需要满足以下场景:

  系统是分布式系统,java的锁已经锁不住了。操作资源,例如数据库中的唯一用户数据。同步访问,即多个进程同时操作共享资源。答:举一个我在项目中使用分布式锁的例子:

  积分可以在许多系统中使用,例如信用卡、电子商务网站、积分兑换礼品等。在这里,“消费点”的操作是一个典型的需要使用锁的场景。

  00-1010以积分兑换礼品为例。完整的积分消费流程简单地分为3个步骤:

  A1:用户选择商品,发起交易并提交订单。

  A2:系统读取用户剩余积分,判断用户当前积分是否足够。

  A3:扣除用户积分。

  00-1010系统也可以分为三个步骤:

  B1:计算用户当天获得的积分。

  B2:阅读用户的原始积分

  B3:在原来的分数上加上应得的分数。

  那么问题来了,如果用户的消费积分和用户的累计积分同时发生(并且用户积分同时操作)会怎么样?

  假设:用户在消费积分的同时,恰好线下任务在计算积分并给用户(比如根据用户当天的消费金额)。这两件事同时进行。以下逻辑有点绕,耐心理解。

  u有1000积分(记录用户积分的数据可以理解为共享资源),这个兑换要花999积分。

  解锁情况:当事件A的程序执行到步骤2读取积分时,操作A:2读取的结果是1000分。判断剩余积分足够本次兑换,然后执行步骤A:3扣除积分(1000-999=1)。正常结果应该是用户还是1分。但此时,事件B也正在执行。这次给用户U 100分,两个线程同时(同步访问)不加锁。会有以下可能,A:2-B33602-A:3-B33603,在A33603完成之前(扣分,1000-999

  有人说怎么可能这么熟练的同时操作用户点,cpu这么快。只要用户足够多,并发足够大,墨菲定律迟早会生效。上述bug出现只是时间问题,黑产行业有可能会被这个bug卡住。薅羊毛,这个时候,作为一个开发商,有必要用解锁来解决这个隐患。

  (写代码是一件严谨的事情!)

  Java本身提供了两种内置锁的实现,一种是JVM实现的同步锁和JDK提供的锁,很多原子操作类是线程安全的。当您的应用程序是独立或单进程应用程序时,您可以使用这两种锁来实现锁。

  但是,目前互联网公司的系统几乎都是分布式的。这时候Java自带的synchronized or Lock就不能满足分布式环境下锁的要求了,因为代码会部署在多台机器上。为了解决这个问题,分布式锁应运而生。分布式锁的特点是有多个进程,内存不能在多个物理机器之间共享。常见的解决方案是基于内存层的介入,落地方案是基于Redis的分布式锁或者ZooKeeper分布式锁。

  不能分析的更详细,面试官不满意?)

  

引言

 

  00-1010问题分析:目前分布式锁的实现方式主要有两种:1。基于Rediscoluster模式。2.基于Zookeeper集群模式。

  优先考虑这两种,应付面试基本没有问题。

  答:

  lass="maodian">

  

1、基于Redis的分布式锁

方法一:使用setnx命令加锁

 

  

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {// 第一步:加锁 Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 第二步:设置过期时间 jedis.expire(lockKey, expireTime); }}

代码解释:

 

  setnx命令,意思就是 set if not exist,如果lockKey不存在,把key存入Redis,保存成功后如果result返回1,表示设置成功,如果非1,表示失败,别的线程已经设置过了。

  expire(),设置过期时间,防止死锁,假设,如果一个锁set后,一直不删掉,那这个锁相当于一直存在,产生死锁。

  (讲到这里,我还要和面试官强调一个但是)

  思考,我上面的方法哪里与缺陷?继续给面试官解释…

  加锁总共分两步,第一步jedis.setnx,第二步jedis.expire设置过期时间,setnx与expire不是一个原子操作,如果程序执行完第一步后异常了,第二步jedis.expire(lockKey, expireTime)没有得到执行,相当于这个锁没有过期时间,有产生死锁的可能。正对这个问题如何改进?

  改进:

  

public class RedisLockDemo { private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {// 两步合二为一,一行代码加锁并设置 + 过期时间。 if (1 == jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)) { return true;//加锁成功 } return false;//加锁失败 }}

代码解释:

 

  将加锁和设置过期时间合二为一,一行代码搞定,原子操作。

  (没等面试官开口追问,面试官很满意了)

  

 

  

3、面试官:那解锁操作呢?

答:释放锁就是删除key

 

  

 

  

使用del命令解锁

public static void unLock(Jedis jedis, String lockKey, String requestId) { // 第一步: 使用 requestId 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 第二步: 若在此时,这把锁突然不是这个客户端的,则会误解锁 jedis.del(lockKey); }}

代码解释:通过 requestId 判断加锁与解锁是不是同一个客户端和 jedis.del(lockKey) 两步不是原子操作,理论上会出现在执行完第一步if判断操作后锁其实已经过期,并且被其它线程获取,这是时候在执行jedis.del(lockKey)操作,相当于把别人的锁释放了,这是不合理的。当然,这是非常极端的情况,如果unLock方法里第一步和第二步没有其它业务操作,把上面的代码扔到线上,可能也不会真的出现问题,原因第一是业务并发量不高,根本不会暴露这个缺陷,那么问题还不大。

 

  但是写代码是严谨的工作,能完美则必须完美。针对上述代码中的问题,提出改进。

  代码改进:

  

public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call(get, KEYS[1]) == ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }}

代码解释:

 

  通过 jedis 客户端的 eval 方法和 script 脚本一行代码搞定,解决方法一中的原子问题。

  

 

  

3、面试官:

 

  

基于 ZooKeeper 的分布式锁实现原理

答:还是积分消费与积分累加的例子:事件A和事件B同时需要进行对积分的修改操作,两台机器同时进行,正确的业务逻辑上让一台机器先执行完后另外一个机器再执行,要么事件A先执行,要么事件B先执行,这样才能保证不会出现A:2 -> B:2 -> A:3 -> B:3这种积分越花越多的情况(想到这种bug一旦上线,老板要生气了,我可能要哭了)。

 

  怎么办?使用 zookeeper 分布式锁。

  一个机器接收到了请求之后,先获取 zookeeper 上的一把分布式锁(zk会创建一个 znode),执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等待,等第一个机器执行完了方可拿到锁。

  使用 ZooKeeper 的顺序节点特性,假如我们在/lock/目录下创建3个节点,ZK集群会按照发起创建的顺序来创建节点,节点分为/lock/0000000001、/lock/0000000002、/lock/0000000003,最后一位数是依次递增的,节点名由zk来完成。

  ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。

  根据ZK中节点是否存在,可以作为分布式锁的锁状态,以此来实现一个分布式锁,下面是分布式锁的基本逻辑:

  客户端调用create()方法创建名为/dlm-locks/lockname/lock-的临时顺序节点。客户端调用getChildren(lockname)方法来获取所有已经创建的子节点。客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,如果是第一,那么就认为这个客户端获得了锁,在它前面没有别的客户端拿到锁。如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁。释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可,不过也仍需要考虑删除节点失败等异常情况。

  

 

  

额外补充

分布式锁还可以从数据库下手解决问题

 

  

 

  

方法一:

利用 Mysql 的锁表,创建一张表,设置一个 UNIQUE KEY 这个 KEY 就是要锁的 KEY,所以同一个 KEY 在mysql表里只能插入一次了,这样对锁的竞争就交给了数据库,处理同一个 KEY 数据库保证了只有一个节点能插入成功,其他节点都会插入失败。

 

  这样 lock 和 unlock 的思路就很简单了,伪代码:

  

def lock : exec sql: insert into locked—table (xxx) values (xxx) if result == true : return true else : return falsedef unlock : exec sql: delete from lockedOrder where order_id=order_id

 

  

方法二:

使用流水号+时间戳做幂等操作,可以看作是一个不会释放的锁。

 

  

 

  

总结

针对分布式锁的两种实现方法,使用哪种需要取决于业务场景,如果系统接口的读写操作完全是基于内存操作的,那显然使用Redis更合适,Mysql表锁or行锁明显不合适。同样是基于内存的 Redis锁 和 ZK锁具体选用哪一种,要根据是否有具体环境和架构师对哪种技术更为了解,原则就是选你最了解的,目的是能解决问题。

 

  以上就是分布式面试分布式锁实现及应用场景的详细内容,更多关于分布式锁实现及应用场景的资料请关注盛行IT其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: