Zookeeper分布式锁

92 阅读13分钟

分布式协调技术

分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让它们有序的去访问某种临界资源,防止造成"脏数据"的后果。 在这里插入图片描述

在这图中有三台机器,每台机器各跑一个应用程序。然后我们将这三台机器通过网络将其连接起来,构成一个系统来为用户提供服务,对用户来说这个系统的架构是透明的,他感觉不到我这个系统是一个什么样的架构。那么我们就可以把这种系统称作一个分布式系统。

在这个分布式系统中如何对进程进行调度,我假设在第一台机器上挂载了一个资源,然后这三个物理分布的进程都要竞争这个资源,但我们又不希望他们同时进行访问,这时候我们就需要一个协调器,来让他们有序的来访问这个资源。这个协调器就是我们经常提到的那个锁,比如说"进程-1"在使用该资源的时候,会先去获得锁,"进程1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。这个分布式锁也就是我们分布式协调技术实现的核心内容。

分布式锁

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个分布式锁。

在这里插入图片描述

- 成员变量 A 存在 JVM1、JVM2、JVM3 三个 JVM 内存中 - 成员变量 A 同时都会在 三个JVM 分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的 - 不是同时发过来,三个请求分别操作三个不同 JVM 内存区域的数据,变量 A 之间不存在共享,也不具有可见性,处理的结果也是不对的 注:该成员变量 A 是一个有状态的对象

如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题,这就是分布式锁要解决的问题

在这里插入图片描述

  • 在分布式环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取与释放锁
  • 高性能的获取与释放锁
  • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

Zookeeper: 利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列,Zookeeper设计的初衷,就是为了实现分布式锁服务

Zookeeper

Zookeeper是一种分布式协调服务,用于管理大型主机,在分布式环境中协调和管理服务是一个复杂的过程,Zookeeper通过其简单的架构和API解决这个问题,Zookeeper允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

在这里插入图片描述

Zookeeper 的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做 Znode。

但是不同于树的结点,Znode的引用方式是路径引用,类似于文件路径:/动物/猫

这样的层级结构,让每一个Znode节点拥有唯一的路径,就像命名空间一样对不同信息做出清晰的隔离

在这里插入图片描述

持久节点: 默认的节点类型,创建节点的客户端与Zookeeper断开连接后,该节点依旧存在

顺序节点: 顺序节点就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号

临时节点: 和持久节点相反,当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除

临时循序节点: 临时顺序节点结合临时节点和顺序节点的特点,在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号,当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除

在这里插入图片描述

data : Znode存储的数据信息

ACL : 记录Znode的访问权限,即哪些人或哪些IP可以访问本节点

stat : 包含Znode的各种元数据,比如事务 ID、版本号、时间戳、大小等

child: 当前节点的子节点引用

这里需要注意一点,Zookeeper 是为读多写少的场景所设计。Znode 并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点的数据最大不能超过 1MB

Zookeeper的一致性

Zookeeper身为分布式系统协调服务,如果自身挂了如何处理呢?为了防止单机挂掉的情况,Zookeeper维护了一个集群。

在这里插入图片描述

Zookeeper Service集群是一主多从结构。

在更新数据时,首先更新到主节点(这里的主节点是指服务器,不是Znode),再同步到从节点。

在读数据时,直接读取任意从节点。

为了保证主从节点的数据一致性,Zookeeper采用了ZAB协议,这种协议非常类似于一致性算法Paxos和Raft。

ZAB

Zookeeper Atomic Broadcast,有效解决了Zookeeper集群崩溃恢复,以及主从同步数据的问题。

ZAB协议定义的三种节点状态

  • Looking:选举状态
  • Following:Follower节点(从节点)所处的状态
  • Leading:Leading节点(主节点)所处状态

最大ZXID

最大ZXID也就是节点本地的最新事务编号,包含epoch和计数两部分,epoch是纪元的意思,相当于Raft算法选主时候的term

ZAB崩溃恢复

假如Zookeeper当前的主节点挂掉了,集群会进行崩溃恢复,ZAB的崩溃恢复分成三个阶段

Leader election

选举阶段,此时集群中的节点处于 Looking 状态。它们会各自向其他节点发起投票,投票当中包含自己的服务器 ID 和最新事务 ID(ZXID)。

接下来,节点会用自身的 ZXID 和从其他节点接收到的 ZXID 做比较,如果发现别人家的 ZXID 比自己大,也就是数据比自己新,那么就重新发起投票,投票给目前已知最大的 ZXID 所属节点。

每次投票后,服务器都会统计投票数量,判断是否有某个节点得到半数以上的投票。如果存在这样的节点,该节点将会成为准 Leader,状态变为 Leading。其他节点的状态变为 Following。

Discovery

发现阶段,用于在从节点中发现最新的 ZXID 和事务日志。或许有人会问:既然 Leader 被选为主节点,已经是集群里数据最新的了,为什么还要从节点中寻找最新事务呢?

这是为了防止某些意外情况,比如因网络原因在上一阶段产生多个 Leader 的情况。所以这一阶段,Leader 集思广益,接收所有 Follower 发来各自的最新 epoch 值。Leader 从中选出最大的 epoch,基于此值加 1,生成新的 epoch 分发给各个 Follower。

各个 Follower 收到全新的 epoch 后,返回 ACK 给 Leader,带上各自最大的 ZXID 和历史事务日志。Leader 选出最大的 ZXID,并更新自身历史日志。

Synchronization

同步阶段,把 Leader 刚才收集得到的最新历史事务日志,同步给集群中所有的 Follower。只有当半数 Follower 同步成功,这个准 Leader 才能成为正式的 Leader。自此,故障恢复正式完成。

ZAB数据写入

ZAB的数据写入涉及到Broadcast阶段,简单来说,就是Zookeeper常规情况下更新数据的时候,由Leader广播到所有的Follower,其过程如下

  • 客户端发出写入数据请求给任意Follower
  • Follower把写入数据请求转发给Leader
  • Leader采用二阶段提交方式,先发送Propose广播给Follower
  • Follower接到Propose消息,写入日志成功后,返回ACK消息给Leader
  • Leader接到半数以上ACK消息,返回成功给客户端,并且广播Commit请求给FOllower

ZAB协议既不是强一致性,也不是弱一致性,而是处于两者之间的顺序一致性,它依靠事务ID和版本号,保证了数据的更新和读取是有序的。

Zookeeper的应用场景

分布式锁

这是Zookeeper的设计初衷,利用Zookeeper的临时顺序节点,可以轻松实现分布式锁

服务注册与发现

利用Znode和Watcher可以实现分布式服务的注册与发现,最著名的应用就是阿里的分布式RPC框架Dubbo

共享配置和状态信息

Redis的分布式解决方案Codis,就利用了Zookeeper来存放数据路由表和codis-proxy节点的元信息。同时codis-config发起的命令都会通过Zookeeper同步到各个存活的codis-proxy。

此外,Kafka、HBase、Hadoop,也都依靠Zookeeper同步节点信息,实现高可用

Zookeeper分布式锁原理

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

在这里插入图片描述

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

在这里插入图片描述

这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下再创建一个临时顺序节点 Lock2。

在这里插入图片描述

Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现节点 Lock2 并不是最小的。

于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态。

在这里插入图片描述

这时候,如果又有一个客户端 Client3 前来获取锁,则在 ParentLock 下载再创建一个临时顺序节点 Lock3。

在这里插入图片描述

Client3 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock3 是不是顺序最靠前的一个,结果同样发现节点 Lock3 并不是最小的。

于是,Client3 向排序仅比它靠前的节点 Lock2 注册 Watcher,用于监听 Lock2 节点是否存在。这意味着 Client3 同样抢锁失败,进入了等待状态。

在这里插入图片描述

这样一来,Client1 得到了锁,Client2 监听了 Lock1,Client3 监听了 Lock2。这恰恰形成了一个等待队列。

Zookeeper配置说明

Zookeeper的三种工作模式

  • 单机模式 : 存在单点故障
  • 集群模式 : 在多台机器上部署Zookeeper集群,适合线上环境使用
  • 伪集群模式 : 在一台机器上同时运行多个Zookeeper实例,任然有单点故障问题,当然其中配置的信息要错开,适合实验环境模拟集群

Zookeeper的三种端口号

**2181 :** 客户端连接Zookeeper集群使用的监听端口号

3888 : 选举leader使用

2888 : 集群内机器通讯使用(Leader 和 Follower之间数据同步使用的端口号,Leader监听此端口)

Zookeeper配置

单机模式配置文件

# 客户端连接Zookeeper服务器的端口,Zookeeper会监听这个端口,接受客户端的访问请求
clientPort=2181
# Zookeeper 保存数据的目录
dataDir=/data 
# Zookeeper 保存日志的目录
dataLogDir=/datalog 
# 作为Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,每隔tickTime时间就会发送一个心跳
tickTime=2000 

集群模式配置文件

clientPort=2181
dataDir=/data
dataLogDir=/datalog
tickTime=2000
initLimit=5
syncLimit=2
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
maxClientCnxns=60
server.1=192.168.0.1:2888:3888
server.2=192.168.0.2:2888:3888
server.3=192.168.0.3:2888:3888
  • initLimit : 配置Zookeeper接受客户端(这里所说的客户端不是用户连接Zookeeper服务器的客户端,而是Zookeeper服务器集群中链接到Leader的Follower)初始化连接时最长能忍受多少个心跳时间间隔数,当已经超过initLimit(默认为10)个心跳的时间(也就是tickTime)长度后Zookeeper服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败,总的时间长度就是5 * 2000 = 10秒

  • syncLimit : 配置Leader与Follower之间发送消息,请求和应答时间长度,最长不能超过多少个tickTime的时间长度,总的时间长度就是2 * 2000 = 4 秒

  • 定时清理(Zookeeper 从 3.4.0 开始提供了自动清理快照和事务日志的功能)以下两个参数配合使用:

    • autopurge.purgeInterval: 指定了清理频率,单位是小时,需要填写一个 1 或更大的整数,默认是 0,表示不开启自己清理功能。
    • autopurge.snapRetainCount: 指定了需要保留的文件数目。默认是保留 3 个。
  • maxClientCnxns: 限制连接到 Zookeeper 的客户端的数量,限制并发连接的数量,它通过 IP 来区分不同的客户端。此配置选项可以用来阻止某些类别的 Dos 攻击。将它设置为 0 或者忽略而不进行设置将会取消对并发连接的限制。

  • server.A=B:C:D: 其中 A 是一个数字,表示这个是第几号服务器。B 是这个服务器的 IP 地址。C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口(2888);D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口(3888)。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

注意:server.A 中的 A 是在 dataDir 配置的目录中创建一个名为 myid 的文件里的值(如:1)

在这里插入图片描述