菜单

分布式锁相关问题

duckflew
发布于 2022-07-15 / 122 阅读
0
0

分布式锁相关问题

为什么需要分布式锁

与分布式锁对应的是单机锁通常来说, 在涉及到多线程程序的时候,为了避免同时操作一个共享变量产生数据问题,通常会使用锁来进行互斥和同步操作,保证共享变量的正确行,使用范围局限在一个进程中

如果是多个进程都需要操作一个共享资源,如何实现互斥和同步?

现在很多的业务都是微服务架构,集群部署,如果多个服务都需要修改mysql中的同一行数据,为了避免错误,就需要引入分布式锁

简单来说,分布式锁就是将加锁和解锁这一步从进程中独立出来,化身为一个单独的服务,专门给其他的服务来提供“锁服务”

这个独立的系统,可以考虑采用Mysql Redis Zookeepr 甚至Consule等等分布式组件或者中间件都可以

一般来说采用比较广泛的是Redis和Zookeeper

分布式锁如何实现?

以redis为例,其实很简单,只需要在redis中维护一个lock变量,不同的客户端申请加锁, 就检查是否存在lock变量,存在说明锁正在被持有,如果不存在则新建,这是一个最简单的实现

SETNX lock 1 ## client1 操作
SETNX lock 1 ## client2操作
(integer) 0 失败

但是这样做会有很多问题

  • 程序处理业务逻辑的时候发生异常,例如死循环之类的,或者业务复杂,没有释放锁
  • 进程挂了,没有机会释放

这样的情况下,别的进程将永远没有几乎拿到锁

如何避免这种情况?

可以考虑在申请锁的同时设置过期时间, 这个时间可以根据业务进行的时间长度来估计,时间一到,redis自动删除lock 释放锁

不过这样做的弊端也很显然

我们无法准确估计业务的时间,无法预判业务的突发情况

出现的问题可能有很多种 例如

  • 客户端1 在持有锁的时候在进行别的业务,此时锁过期了,被客户端2持有,这就出现多个进程持有一把锁的情况 (A)

  • 还是上面的情况,只是进程1在业务结束之后释放锁,但是此时的锁是被进程2持有的,也就是说 进程1 释放了别的进程的锁 (B)

  • 设置锁和设置过期时间必须保证原子性操作

    这种情况在redis2.6.12后可以通过一条命令来避免了

    SET lock 1 EX 10 NX
    

    对于问题A 可以考虑适当延长过期时间 当然这也是缓兵之计,没有解决实际的问题 即 无法准确估计业务时间和突发情况,所以可以再换一个思路,引入一个守护线程,定时检测锁是不是马上要过期,如果要过期的时间临近,但是任务还远没有结束,那就进行一个续期的操作 重新设置一个过期的时间。 在Java中,这种实现方案已经有了 —Redisson 这个库已经封装号了,这个库已经采用了自动续期的方案来避免锁过期的问题,这个守护线程一般叫做看门狗线程

对于问题B 可以考虑引入进程的唯一标识, 可以是进程ID uuid等等都可以 如果不是自己的锁 就不进行操作 但是同样的 在get 操作 和set 操作之间 也就是在判断锁是否是自己拥有的 和改变这个锁之间 也必须是原子性操作 这个可以在redis中引入lua脚本,让redis单线程执行

分布式锁本身安全吗?

注意这里我说的是分布式锁本身,考虑到如今动不动都要引入的集群,分布式部署等等方案,那么我们前面提到的锁服务很可能也不是单机部署的 ,redis的集群模式有很多种,哨兵,主从,完全集群,伪集群等等

那么主从redis发生切换或者数据在同步的时候,这个分布式锁系统还安全吗? 其实不用太多考虑,肯定是不安全的,无论多么牛逼的主从数据库,数据同步之时总是有间隔的,就无法绝对安全,换句话说,无法做到绝对的数据同步 所以在引入Redis副本的时候,分布式锁还是会收到影响

对于这种情况,redis的作者提出了一种解决方案,Redlock

Redlock的思路并非将分布式锁服务部署在Redis集群上,而是部署在多个单独的Redis实例上

对于这多个实例加锁和释放锁的操作,流程比较复杂,有几个重点如下

  • 客户端在多个Redis实例上面申请加锁
  • 必须保证大多数的节点加锁成功
  • 大多数节点加锁所要消耗的时间,需要小于锁设置的过期时间
  • 释放锁,需要想所有的节点发起释放的请求

但是理论上这种方案还是无法完全实现准确无误,其实Redis作者也提到了。

这里说到一个小插曲 也就是分布式系统中的NPC问题,

  • N Network Delay 网络延迟
  • P Process Pause 进程暂停
  • C Clock Drift 时钟漂移

这是所有分布式系统都存在的问题。

回到正题,Redis作者解释了 这三种情况,首先时钟问题,上面提到的多实例的Redis解决方案不需要完全的一致时钟,只需要大体一致就行

只要误差范围不超过锁的过期时间即可,这也是符合现实情况的

对于时钟跳跃的情况:保证机器的时钟不会大幅度跳转,这是运维应该做到的工作

另外还有一些极端的情况 这并非Redlock需要面对的问题,而是 其他的锁服务例如zookeeper 都有类似的问题

Zookeeper实现的锁安全吗?

zokeeper的分布式锁是这样实现的:

  • 客户端1和客户端2都试图创建一个临时节点 例如/lock
  • 如果客户端1 先到达了,则加锁成功,客户端2加锁失败
  • 客户端1删除/lock节点,释放掉锁

而且zookeeper的优势在 如果客户端1崩溃了 那么zookeeper会自动删除掉这个锁节点, 这是zookeeper临时节点的特性之一

当然这样也是有问题的,Zookeeper能做到自动删除 主要是因为它的心跳机制,相当于维护了一个会话session 这个session会维护链接,如果zk长期收不到客户端的心态,就会认为这个session过期了 也会删除掉lock

所以zk也无法保证长时间GC 网络延迟等等问题

但是这样可以引出一些对比

Zookeeper和Redis分布式锁的优劣对比

zk的优点

  • 不需要考虑过期时间问题
  • watch机制,如果加锁失败,可以等待锁释放,实现乐观锁

劣势:

  • 性能不如redis
  • 部署和运维的成本比较高
  • zk和客户端长期失联会导致锁被释放

评论