Zookeep 分布式锁

什么是分布式锁

概述

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

分布式锁应具备的条件

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

分布式锁有哪些实现

  • Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。
  • Redis:利用 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
  • Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
  • Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

Redis分布式锁的实现

加锁

setnx(lock_sale_商品ID, 1)

当一个线程实行 setnx 返回 1,说明 key 原本不存在,该线程成功的到了锁;当一个线程执行setnx 返回 0,说明 key 已经存在,该线程抢锁失败。

解锁

当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下:

del(lock_sale_商品ID)

释放锁后,其他线程就可以继续执行 setnx 命令来获得锁。

锁超时

如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将永远被锁住(死锁),别的线程再也别想进来。所以,setnxkey 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx 不支持超市参数,所以需要额外的指令,伪代码如下:

expire(lock_sale_商品ID, 30)

综合伪代码

if(setnx(lock_sale_商品ID, 1) == 1){
    expire(lock_sale_商品ID, 30)
    try {
        do something
    } finally {
        del(lock_sale_商品ID)
    }
}

存在什么问题

setnxexpire 的非原子性

setnx 不支持传入超时时间,但可以使用 set 指令,并增加可选参数:

set(lock_sale_商品ID, 1, 30, NX)

del 导致误删

可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。具体实现:可以在加锁的时候把当前的线程ID当做 value,并在删除之前验证 key 对应的 value 是不是自己线程的ID。

加锁:

String threadId = Thread.currentThread().getId()
set(key, threadId, 30, NX)

解锁: 导致新问题:判断和释放锁是两个独立操作,不是原子性的。

if(threadId.equals(redisClient.get(key))){
    del(key)
}

对于分布式锁,应减少死锁的可能,并保证操作的原子性

什么是Zookeeper

概述

Zookeeper 是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。Zookeeper 通过极其简单的架构和 API 解决了这个问题。

Zookeeper 的数据模型

Zookeeper 的数据模型很像数据结构中的树,也很像文件系统的目录。

树是由节点组成的,Zookeeper 的数据存储也是同样基于节点,这种节点佳作Znode。对于不同树的节点,Znode 的引用方式是路径引用,类似于文件路径:/动物/猫 /汽车/宝马

Znode包含哪些元素

  • data:Znode 存储的数据信息。
  • ACL:记录 Znode 的访问权限,即那些人或哪些IP可以访问本节点。
  • stat:包含 Znode 的各种元数据,比如事务ID、版本号、时间戳、大小等。
  • child:当前节点的子节点引用。

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

Zookeeper 的基本操作

  • 创建节点 create
  • 删除节点 delete
  • 判断节点是否存在 exists
  • 获得一个节点的数据 getData
  • 设置一个节点的数据 setData
  • 获取节点下的所有节点 getChildren

其中,exests , getData , getChildren 属于读操作。Zookeeper 客户端在请求读操作的时候,可以选择是否设置 Watch

Zookeeper 的事件通知

我们可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就是调用了 create , delete ,setData 等方法的时候,将会出发 Znode 上注册的对应事件,请求 Watch 的客户端会接受到异步通知。

具体交互过程:

  • 客户端调用 getData 方法,watch 参数是 true。服务端接收到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
  • 当 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。

Zookeeper 的一致性

Zookeeper 身为分布式系统协调服务,为了防止单机挂掉的情况,Zookeeper 维护了一个集群。

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

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

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

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

什么是ZAB协议

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

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

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

最大 ZXID

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

ZAB 崩溃恢复

加入 Zookeeper 当前主节点挂掉了,集群会进行奔溃恢复。ZAB 的奔溃恢复分成三个阶段:

  • Leader Election

    1. 选举阶段,此时集群中的节点处于 Looking 状态。它们会向其他节点发起投票,投票当中包含自己的服务器ID 和最新事务ID(ZXID)。
    2. 接下来,节点会用自身的 ZXID 和从其他节点接收到的 ZXID 作比较,如果发现别人家的 ZXID 比自己大,也就是数据比自己新,那么就重新发起投票,投票给目前已知最大的ZXID所属节点。
    3. 每次投票后,服务器都会统计投票数量,判断是否有某个节点得到半数以上的投票。如果存在这样的节点,该节点就会成为准 Leader,状态变成 Leading。其他节点的状态变为 Following。
  • Discovery

    发现阶段,用于在从节点中发现最新的 ZXID和事务日志(为了防止某些意外的情况,比如因网络原因在上一阶段产生了多个Leader的情况)。

    1. 在这一阶段,Leader 集思广益,接受所有Follower发来各自的最新 epoch 值。Leader从中选出最大的epoch,基于此值加 1,生成新的 epoch 分发给各个 Follower。
    2. 各个 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 的临时顺序节点,可以轻松实现分布式锁。

服务注册和发现

利用 Znode 和 Watcher,可以实现分布式服务的注册和发现。

共享配置和状态信息

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

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

Zookeeper 分布式锁

什么是临时顺序节点

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

Znode 分为四种类型:

  • 持久节点 Persistent

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

  • 持久节点顺序节点 Persistent_Sequential

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

  • 临时节点 Ephemeral

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

  • 临时顺序节点 Ephemeral_Sequential

    临时顺序节点结合 临时节点和顺序节点的特点:在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 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。这恰恰形成了一个等待队列。

释放锁

释放锁的分为两种情况:

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

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

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

  • 获得锁的 Client1 在执行任务过程中,如果崩溃,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。
  • 由于 Client2 一致监听着 Lock1 的存在状态,当 Lock1 节点被删除, Client2 会立即收到通知。
  • 这时候Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章地获得了锁。

Zookeeper 和 Redis 分布式锁的比较

分布式锁 优点 缺点
Zookeeper 1. 有封装好的框架,容易实现。
2. 有等待锁的队列,大大提升抢锁效率。
添加和删除节点性能低。
Redis Set 和 Del 指令的性能较高。 1. 实现复杂,需要考虑超时、原子性、误删等情形。
2. 没有等待锁的队列,只能在客户端自旋来等锁,效率低下。

本文整理自 (https://www.funtl.com/zh/apache-dubbo-zookeeper/)

原文地址:https://www.cnblogs.com/jianminglin/p/11442105.html

时间: 2024-10-09 18:18:37

Zookeep 分布式锁的相关文章

分布式任务&分布式锁(li)

目前系统中存在批量审批.批量授权等各个操作,批量操作中可能因为处理机器.线程不同,造成刷新缓存丢失授权等信息,如批量审批同一用户权限多个权限申请后,流程平台并发的发送多个http请求到acl不同服务器,a机器处理了授权a,b机器同时处理了授权b,然后刷新用户缓存.因为在事务里彼此看不见对方提交的数据,刷新时又完全从db中读取要刷新的数据,就造成了互相丢失对方的数据.因此,需要一个分布式锁工具,来协调各个机器.线上的工作同步问题. 分布式锁千万不能用ReentrantLock,因为它的lock和u

06.Curator分布式锁

锁:分布式的锁全局同步,这意味着任何一个时间点不会有两个客户端都拥有相同的锁. 1.可重入锁Shared Reentrant Lock 首先我们先看一个全局可重入的锁(可以多次获取,不会被阻塞).Shared意味着锁是全局可见的,客户端都可以请求锁.Reentrant和JDK的ReentrantLock类似,意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞. 1.可重入锁相关类介绍 它是由类InterProcessMutex来实现.它的主要方法: // 构造方法 public Inte

分布式锁的实现思路

为什么要用分布式锁 分布式系统,需要采用集群,多个服务之间可能需要用到共享数据(redis或者数据库),这时,JVM内的锁已经不能满足跨JVM的需求,因此需采用外部的锁机制 如何实现分布式锁 个人理解,不管是java提供的锁还是其他锁,就其思想,就是要有一个大家公用的媒介,作为标识,去控制对于共享资源的操作,因此,分布式系统中,共用媒介便可以作为分布式锁,例如:zookeeper,redis等等,甚至文件都可以 实现分布式锁需要注意的问题 1.获取锁需要互斥 2.有自动失效的机制,防止因为持锁线

redisson实现分布式锁原理

Redisson分布式锁 之前的基于注解的锁有一种锁是基本redis的分布式锁,锁的实现我是基于redisson组件提供的RLock,这篇来看看redisson是如何实现锁的. 不同版本实现锁的机制并不相同 引用的redisson最近发布的版本3.2.3,不同的版本可能实现锁的机制并不相同,早期版本好像是采用简单的setnx,getset等常规命令来配置完成,而后期由于redis支持了脚本Lua变更了实现原理. <dependency> <groupId>org.redisson&

高级java必会系列一:zookeeper分布式锁

方案1: 算法思路:利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁.解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁.特点:这种方案的正确性和可靠性是ZooKeeper机制保证的,实现简单.缺点是会产生"惊群"效应,假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,仅仅有一个客户端得到锁.方案2:算法思路:临时顺序节点实现共享锁        客户端调用create(

基于zookeeper的分布式锁实现 【转】

工作中需要写一个定时任务,由于是集群环境,自然而然想到需要通过分布式锁来保证单台执行..相信大家都会想到使用zk来实现对应的分布式锁.下面就简单介绍一下几种实现 准备工作 有几个帮助类,先把代码放上来 ZKClient 对zk的操作做了一个简单的封装 Java代码 ZKUtil 针对zk路径的一个工具类 Java代码 NetworkUtil 获取本机IP的工具方法 Java代码 --------------------------- 正文开始  -------------------------

基于zookeeper的分布式锁实现 【转载】

工作中需要写一个定时任务,由于是集群环境,自然而然想到需要通过分布式锁来保证单台执行..相信大家都会想到使用zk来实现对应的分布式锁.下面就简单介绍一下几种实现 准备工作 有几个帮助类,先把代码放上来 ZKClient 对zk的操作做了一个简单的封装 Java代码 ZKUtil 针对zk路径的一个工具类 Java代码 NetworkUtil 获取本机IP的工具方法 Java代码 --------------------------- 正文开始  -------------------------

使用zookeeper实现分布式锁

简介: 核心是解决资源竞争的问题 分布式系统中经常需要协调多进程或者多台机器之间的同步问题,得益于zookeeper,实现了一个分布式的共享锁,方便在多台服务器之间竞争资源时,来协调各系统之间的协作和同步. 实现1: ConcurrentTest: package com.concurrent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.con

ZooKeeper场景实践:(7) 分布式锁

1.基本介绍 分布式锁是控制分布式系统之间同步访问共享资源的一种方式,需要互斥来防止彼此干扰来保证一致性.利用Zookeeper的强一致性可以完成锁服务.Zookeeper的官方文档是列举了两种锁,独占锁和共享锁.独占锁保证任何时候都只有一个进程能或者资源的读写权限.共享锁可以同时有多个读,但是同一时刻最多只能有一个写,读和写是互斥的. 2.场景分析 我们准备来实现互斥的锁,按照官网的思路,给定一个锁的路径,如/Lock,所有要申请这个锁的进程都在/Lock目录下创建一个/Lock/lock-的