12.分布式锁&分布式事务面试题

531 阅读10分钟

一、分布式锁

1. 分布式锁有哪些实现方式?

  1. 基于数据库实现

  • 基于表记录(创建锁表,创建字段唯一约束,获取锁时插入数据,解锁删除该数据)

  • 乐观锁(通过添加版本号、时间戳)

  • 悲观锁(利用行锁,在select语句后加上 for update)

  1. 基于redis实现

  • 基于redis

    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "cmx",10, TimeUnit.SECONDS);
    //result为ture,加锁成功
    

    缺点:

    • 设置锁的时间短了会存在释放别人的锁
    • 设置锁的时间长了会存在服务宕机后一段时间锁无法释放
  • 基于redission

     // 1.获取锁对象
    RLock redissonLock = redisson.getLock(lockKey);
    //2.加锁
     redissonLock.lock();  
    //3.释放锁
    redissonLock.unlock();
    

    优点:

    • 可以实现可重入锁

    • 公平锁

    • 看门狗自动续期

    • 服务宕机自动释放

  1. 基于zookeeper实现

  • 基于临时顺序节点实现分布式锁

    • 加锁:

      1. 在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

      2. Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

      3. 如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

      4. 这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味Client3同样抢锁失败,进入了等待状态。

    • 释放锁:

      两种情况

      1. 任务完成,客户端显示释放

        当任务完成时,Client1会显示调用删除节点Lock1的指令。

      2. 任务执行过程中,客户端崩溃

        获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

2. 分布式锁实现方案对比总结?

  1. 基于数据库实现:最简单;性能不好;并发量少时可用

  2. 基于Redis的Redission方案是主推方案,实现了公平锁、可重入锁、支持redis集群、锁的自动续约,通过简单的配置 ,获取RLock对象, 调用对象.lock()加锁 调用.unlock方法解锁即可,代码侵占非常低;

redis为保证高可用是集群部署的,当把key写入到master节点后,master还未同步到slave节点时master宕机了,原有的slave节点经过选举变为了新的master节点,此时可能就会出现锁失效问题。(redis 是基于AP的)。

  1. 基于zookeeper性能对比redis稍慢一些,强一致性的zk 在leader宕机后会出现短时间的不可用。(zookeeper基于CP)。

3. Redission实现分布式锁RLock的原理?

  1. 加锁机制

加锁底层是基于lua脚本的,lua中第一个if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,就进行加锁。如何加锁呢?很简单,用下面的hset命令:

hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:63 1

此时的myLock锁key的数据结构是:

myLock:{    8743c9c0-0795-4907-87fd-6c719a6b4586:63 1}

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒(默认)

  1. 锁互斥机制 + 自旋机制

如果在这个时候,另一个客户端(客户端2)来尝试加锁,执行了同样的一段lua脚本,会怎样呢?

第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

接着第二个if判断会执行“hexists mylock 客户端id”,来判断myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。

比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。

  1. 可重入加锁机制

上面lua代码第一个if判断不成立,“exists myLock” 会显示锁key已经存在了

第二个if会成立,因为myLock的hash数据结构中包含的客户端1的ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,用hincrby这个命令,对客户端1的加锁次数,累加1:

hincrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:63  1

此时myLock数据结构变为下面这样:

myLock:    {        8743c9c0-0795-4907-87fd-6c719a6b4586:63  2    }
  1. 释放锁机制

执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key。

然后另外的客户端2就可以尝试完成加锁了。

  1. watch dog自动延期机制

  2. 客户端1加锁的myLock默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

    redisson中客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有myLock,那么就会不断的延长myLock的生存时间。

  3. 如果负责存储这个分布式锁的redission节点宕机后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态,为了避免这种情况的发生,redisson提供了一个监控锁的看门狗,它的作用是在redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。

  4. 可以看到,这个加的分布式锁的超时时间默认是30秒.但是还有一个问题,那就是这个看门狗,多久来延长一次有效期呢?

    获取锁成功就会开启一个定时任务,也就是watchdog,定时任务会定期检查去续期;该定时调度每次调用的时间差是internalLockLeaseTime / 3。也就10秒。

  5. 通过源码分析我们知道,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么有效期到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗。

  6. 另外redisson还提供了lock(long leaseTime, TimeUnit unit)可以指定加锁时间的方法。超过leaseTime时间后锁便自动解开了。不会再对锁进行续期!!!

二、分布式事务

1. 说说CAP定理?

一个分布式系统无法同时满足:

  1. 一致性(Consistency)
  2. 可用性(Availability)
  3. 分区容错性(Partition tolerance)

⼀个Web应⽤⾄多只能同时⽀持上⾯的两个属性。因此,我们必须在⼀致性与可⽤性之间做出选择。

2. 说说 BASE 理论?

BASE是Basically Available(基本可⽤)、Soft state(软状态)和 Eventually consistent(最终⼀致性)三个短语的缩写。BASE基于CAP定理演化⽽来,核⼼思想是即时⽆法做到强⼀致性,但每个应⽤都可以根据 ⾃身业务特点,采⽤适当的⽅式来使系统达到最终⼀致性。

  1. Basically Available(基本可⽤)

基本可⽤是指分布式系统在出现不可预知的故障的时候,允许损失部分可⽤性,但不等于系统不可⽤。

  • 响应时间上的损失:当出现故障时,响应时间增加

  • 功能上的损失:当流量⾼峰期时,屏蔽⼀些功能的使⽤以保证系统稳定性(服务降级)、

  1. Soft state(软状态)

指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可⽤性。即允许系统在不同节点的数据副本之间进⾏数据同步的过程存在延时。

  1. Eventually consistent(最终⼀致性)

强调系统中所有的数据副本,在经过⼀段时间的同步后,最终能够达到⼀个⼀致的状态。其本质是需要系统保证最终数据能够达到⼀致,⽽不需要实时保证系统数据的强⼀致性。

三、分布式ID

1. 说说分布式id解决方案

  1. UUID

    优点:生成的主键全局唯一;降低全局节点压力且生成速度块;跨服务器合并数据方便;

    缺点:占用16个字符,占用空间较多;存储在mysql中时,因为不是有序数字,IO随机性大,索引效率下降;

  2. 数据库自增主键

    优点:int或者bigint类型占用空间小;自动递增,生成的id数据连续性好,且查询效率优于字符串。

    缺点:并发性能不高(受限于mysql性能);如果分表分库,则需要改造;自增情况下,如果暴露id,则会导致数据量的泄露。

  3. redis自增

    优点:使用内存,并发性能好

    缺点: 可能存在数据丢失;自增导致数据量泄露(针对自增,可以设置失效时间位1天,并且拼接日期作为唯一标识,这样可以只暴露当天最大数据量,减少影响。未验证过高并发下添加过期策略带来的影响)

  4. 雪花算法

    优点:不依赖外部组件;性能好 缺点(存在的问题):时钟回拨,不同服务器时间戳不一致导致id重复