(面试高频)分布式锁

作者: zzj0990 | 来源:发表于2021-03-24 17:33 被阅读0次

    什么是锁

    普通的锁,即在单机多线程环境下,当多个线程需要访问同一个变量或代码片段时,被访问的变量或代码片段叫做临界区域,我们需要控制线程一个一个的顺序执行,否则会出现并发问题。
    如何控制呢?就是设置一个各个线程都能看的见的标志。然后,每个线程想访问临界区域时,都要先查看标志,如果标志没有被占用,则说明目前没有线程在访问临界区域。如果标志被占用了,则说明目前有线程正在访问临界区域,则当前线程需要等待。
    这个标志,就是
    在单机多线程的java程序中,我们可以使用堆内存中的变量作为标志,因为多线程是共享堆内存的,堆内存中的变量对于各个线程都是可见的。

    分布式锁

    在分布式环境下,即多台计算机,每个计算机上会启动jvm执行程序的运行环境下,如果不同计算机上的线程想访问临界区域时,该怎么办呢?
    前面普通锁的使用堆内存中的变量的方式肯定不适用了。因为在多机环境下,某台计算机上的堆内存中的变量对于其他计算机上的线程肯定是不可见的。那么,根据锁的本质和原理,我们就要找到另外的对于多机上的线程都可见的标志,以它来作为锁,就可以了。这样的锁,就是分布式锁。
    当然,这里只是解释了什么是分布式锁,至于分布式锁该如何实现,其实有多重方式,关键在于要保证锁对多机上的程序是可见的即可。

    分布式锁应该具备哪些条件?

    1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
    2. 高可用的获取锁与释放锁;
    3. 高性能的获取锁与释放锁;
    4. 具备可重入特性;
    5. 具备锁失效机制,防止死锁;
    6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
    

    场景描述

    现实生活中一种常见的场景,抢票。抢票系统一般为适应高并发性,会将服务部署多套(以承载瞬间的大并发场景),分布式锁控制不好,往往会出现超买情况。

    那么我们分以下三种方案来分析分布式锁的设计:

    1. 数据库乐观锁

    • 适用于中小型系统
    • 实现方式有:主键、唯一性索引、版本等等
    • 需要考虑某个分布式系统抢到锁之后,自己挂了,会造成锁无法释放,即死锁。则在库中为该锁设置有效时长
    • 接上场景需要考虑抢到锁的分布式系统,其执行的时间长,可能会比锁的有效期长,即锁过期,任务还未执行完毕。则使用守护线程为锁续时长(看门狗)

    2. Redis

    • 适用于千万级系统

    2.1. setnx
    直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴
    缺点:如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到,从而造成死锁

    2.2. setnx设置一个过期时间
    为了改正2.1。的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,到了过期时间自动释放
    缺点:
    ①setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题;
    ②还会导致当前的线程释放其他线程占有的锁;

    2.3. set k v ex tm nx
    redis官方为了解决2.2.存在的缺点,在v2.8版本为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:
    set key value ex 5 nx
    缺点:
    ①如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁,出现超卖;
    可以通过看门狗,定时续时长来解决

    2.4. 加一个事务id
    对于2.2.中的第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚
    缺点:get key和将value与id比较是两个步骤,不能保证原子性

    2.5. Redis集群(主从)
    会出现系统刚获得分布式锁,此时主Redis宕机(数据还未同步到从节点),也会出现超卖,一次Redis的创始人提出了RedLock算法。

    2.6. RedLock
    这个场景是假设有一个 redis cluster,有 5 个 redis master 实例(一定是奇数个,他们之间无主从关系,都是相互独立的)。然后执行如下步骤获取一把锁:

    a. 获取当前时间戳,单位是毫秒;
    b. 轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
    c. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点` n / 2 + 1`;
    d. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了(设置一个请求过期时间re和锁的过期时间le,同时re必须小于le);
    e. 要是锁建立失败了,那么就依次之前建立过的锁删除;
    f. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
    

    缺点:RedLock原图链接

    RedLock.png

    3. ZooKeeper

    • 利用临时节点特性
      zookeeper的临时节点有两个特性,一是节点名称不能重复,二是会随着客户端退出而销毁,因此直接将key作为节点名称,能够成功创建的客户端则获取成功,失败的客户端监听成功的节点的删除事件
      缺点:所有客户端监听同一个节点,但是同时只有一个节点的事件触发是有效的,造成资源的无效调度
    • 利用顺序临时节点特性
      zookeeper的顺序临时节点拥有临时节点的特性,同时,在一个父节点下创建创建的子临时顺序节点,会根据节点创建的先后顺序,用一个32位的数字作为后缀,我们可以用key创建一个根节点,然后每次申请锁的时候在其下创建顺序节点,接着获取根节点下所有的顺序节点并排序,获取顺序最小的节点,如果该节点的名称与当前添加的名称相同,则表示能够获取锁,否则监听根节点下面的处于当前节点之前的节点的删除事件,如果监听生效,则回到上一步重新判断顺序,直到获取锁。
    • ZK也难以逃脱Full GC的宿命(超出心跳时间,也就释放锁,即出现超卖)

    总结

    • 基于jdk的并发工具自己实现的锁
      优点:不需要引入中间件,架构简单
      缺点:编写一个可靠、高可用、高效率的分布式锁服务,难度较大
    • redis set px nx + 唯一id + lua脚本
      优点:redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
      缺点:依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响,如果发生则需要人工介入。分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能
    • 基于redis的redlock
      优点:可以解决redis集群的同步可用性问题
      缺点:依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率
    • 基于zookeeper的分布式锁
      优点:分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
      缺点:依赖中间件

    没有绝对完美的实现方式,具体要选择哪一种分布式锁,需要结合每一种锁的优缺点和业务特点而定。
    忘记在哪里看到过一句话:过度追求设计完美,是一种心理疾病

    参考

    相关文章

      网友评论

        本文标题:(面试高频)分布式锁

        本文链接:https://www.haomeiwen.com/subject/funqhltx.html