Zookeeper

一、概述

Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目。

从设计模式角度来理解,Zookeeper是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,数据发生变化,Zookeeper负责通知已经注册的观察者做出相应反应。

Zookeeper=文件系统+通知机制

二、特点

  1. Zookeeper是由一个leader,多个follower组成的集群。
  2. 集群中只要有半数以上的节点存活,Zookeeper集群就能正常提供服务。所以Zookeeper适合安装奇数台服务器。
  3. 全局数据一致。每个server保存一份数据副本,client无论连接哪个Server数据都是一致的。
  4. 更新请求顺序执行,来自同一个client的更新请求按其发送的顺序依次执行。
  5. 数据更新原子性。一次更新,要么全部成功要么全部失败。
  6. 实时性,在一定时间范围内,client能读到最新数据。

三、数据结构

Zookeeper是树形结构的文件系统。与Unix文件系统类似,整体可以看做是一棵树,每个节点称作一个ZNode。每个ZNode默认最大存储1MB的数据。每个ZNode都可以用唯一路径标识。

不能存储大量数据。

四、应用场景

1.统一命名服务

分布式环境下,经常需要对应用,服务统一命名便于识别。

Nginx也能实现将IP封装成域名的功能。

2.统一配置管理

分布式环境,一般要求一个集群中,所有节点的配置信息是一致的,如Kafka集群。

配置文件修改后,希望能够快速同步到各个节点上。

Zookeeper实现配置管理:

  1. 将配置信息写入Zookeeper上的一个ZNode。
  2. 各个服务端服务器监听这个ZNode。
  3. 一旦ZNode中的数据被修改,Zookeeper将通知各个客户端服务器。

3.统一集群管理

分布式环境中要求掌握每个节点的实时状态,并可根据状态做出一些调整。

Zookeeper实现实时监控节点变化:

  1. 可将节点信息写入Zookeeper上的一个ZNode。
  2. 监听这个ZNode可以获取它的实时状态变化。

4.服务器节点上下线

  1. 服务器启动注册服务信息,在Zookeeper上创建临时节点。
  2. 客户端获取在线服务列表,并监听列表中的服务信息。
  3. 服务器节点下线。
  4. 服务器节点下线事件通知。
  5. 客户端重新获取服务器列表。

5.软负载均衡

在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求

五、配置参数解读

Zookeeper中的配置文件zoo.cfg参数含义:

  1. tickTime=2000:通信心跳时间,单位毫秒。
  2. initLimit=10:LF初始通信时限。Leader和Follower初始连接时能容忍的最多心跳数(tickTime的数量)。
  3. syncLimit=5:LF同步通信时限。Leader和Follower之间的通信时间如果超过syncTime*tickTime,Leader认为Follower死掉,从服务器列表删除follower。
  4. dataDir:保存zookeeper中的数据。默认的tmp目录是Linux系统的临时目录,会被系统定期删除,所以一般不用默认的tmp目录。
  5. clientPoint=2181:客户端连接端口,通常不做修改。

六、选举机制

1.第一次启动选举机制

(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(

3票),选举无法完成,服务器1状态保持为

LOOKING;

(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING

(3)服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;

(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;

(5)服务器5启动,同4一样当小弟

关键的三个ID:

SID:服务器ID。用来唯一标识一台

ZooKeeper集群中的机器,每台机器不能重

复,和myid一致。

ZXID:事务ID。ZXID是一个事务ID,用来

标识一次服务器状态的变更。在某一时刻,

集群中的每台机器的ZXID值不一定完全一

致,这和ZooKeeper服务器对于客户端“更

新请求”的处理逻辑有关。

Epoch:每个Leader任期的代号。没有

Leader时同一轮投票过程中的逻辑时钟值是

相同的。每投完一次票这个数据就会增加

2.非第一次启动选举机制

( 1)当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举: • 服务器初始化启动。 • 服务器运行期间无法和Leader保持连接。 ( 2)而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态: • 集群中本来就已经存在一个Leader。 对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连 接,并进行状态同步即可。 • 集群中确实不存在Leader。 假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻, 3和5服务器出现故障,因此开始进行Leader选举 SID为1、2、4的机器投票情况:

(EPOCH,ZXID,SID ) ( 1,8,1) ( 1,8,2) ( 1,7,4)

选举Leader规则:

①EPOCH大的直接胜出

②EPOCH相同,事务id大的胜出

③事务id相同,服务器id大的胜出

七、启动停止脚本

服务端的启动与停止

开启命令:bin/zkServer.sh start [config]

停止命令:bin/zkServer.sh stop

脚本:

#!bin/bash

case $1 in
"start") {
   for i in hadoop102 hadoop103 hadoop104
    do
        ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer start"
    done
}
;;
"stop") {
    for i in hadoop102 hadoop103 hadoop104 
    do    
    	ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer stop"
    done
}
;;
"status"){
    for i in hadoop102 hadoop103 hadoop104
    do
    	ssh $1 "/opt/module/zookeeper-3.5.7/bin/zkServer status"
    done
}
;;
esac

客户端的命令行操作

指定服务端启动:bin/zkCli.sh -server hadoop102:2181

创建节点命令 create

create /sanguo/simayi "simayi"

删除节点 delete

delete /sanguo

如果sanguo节点下有子节点则无法删除

删除路径上多个节点 deleteall

deleteall /sanguo

删除sanguo节点及其子节点。

监听节点 get -w /路径

查看节点 ls

查看根目录节点:ls /

查看指定路径的节点:ls /sanguo

退出 quit

修改 set /路径 "名称"

八、节点

1.节点信息

  1. czxid:创建节点的事务ID。(每次修改Zookeeper状态都会产生一个Zookeeper事务ID。事务ID(zxid)是zookeeper中所有修改总的次序。事务ID(zxid)有大小,且全局唯一。)
  2. ctime:znode被创建的毫秒数(从1970开始计算)。
  3. mzxid:zNode最后更新的事务zxid。
  4. mtime:zNode最后修改的毫秒数(从1970开始算起)。
  5. pZxid:zNode最后更新的子节点的zxid。
  6. ccversion:zNode子节点的变化号,zNode子节点修改次数。
  7. dataversion:znode数据变化号
  8. aclversion:zNode访问控制列表的变化号。
  9. ephemeralOwner:如果是临时节点,这个是zNode拥有者的sessionId。如果不是临时节点则是0。
  10. dataLength:zNode的数据长度。
  11. numChildren:zNode的子节点数量。

2.节点类型

持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除

短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除

( 1)持久化目录节点 客户端与Zookeeper断开连接后,该节点依旧存在。例:create /sanguo/wuguo/limeng "limeng" ( 2)持久化顺序编号目录节点 客户端与Zookeeper断开连接后,该节点依旧存 在,只是Zookeeper给该节点名称进行顺序编号。例:create -s /sanguo/wuguo/lusu "lusu" ( 3)临时目录节点 客户端与Zookeeper断开连接后,该节点被删除。例:create -e /sanguo/weiguo/xunyu "xunyu" ( 4)临时顺序编号目录节点 客户端与 Zookeeper 断开连接后 , 该 节 点 被 删 除 , 只 是 Zookeeper给该节点名称进行顺序编号。例:create -e -s /sanguo/weiguo/caocao "caocao"

说明:创建znode时设置顺序标识,znode名称

后会附加一个值,顺序号是一个单调递增的计数

器,由父节点维护

注意:在分布式系统中,顺序号可以被用于

为所有的事件进行全局排序,这样客户端可以通

过顺序号推断事件的顺序

3.监听器及节点删除

1.监听原理详解

  1. 首先要有一个main()线程
  2. 在main线程中创建Zookeeper客户端,这时就会创建两个线 程,一个负责网络连接通信(connet),一个负责监听(listener)。
  3. 通过connect线程将注册的监听事件发送给Zookeeper。
  4. 在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
  5. Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
  6. listener线程内部调用了process()方法。

2.常见监听

  1. 监听节点数据的变化 get path [watch]
    1. 注册监听,监听sanguo节点的数据变化:get -w /sanguo
    2. 注意:注册一次监听只能监听一次数据变化。想再次监听需要重新注册。
  2. 监听子节点增减的变化 ls path [watch]
    1. 注册监听,监听sanguo节点下的节点变化:ls -w /sanguo
    2. 注意:创建一次,监听一次。同上。

4 .IDEA 环境搭建

1)创建一个工程:zookeeper

2)添加pom文件

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.8.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.5.7</version>
    </dependency>
</dependencies>

3)拷贝log4j.properties文件到项目根目录
需要在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在
文件中填入。

log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d   %p [%c] - %m%n

4)创建包名com.atguigu.zk

5)创建类名称zkClient

5. 创建 ZooKeeper 客户端

// 注意:逗号前后不能有空格
private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
private static int sessionTimeout = 2000;
private ZooKeeper zkClient = null;

@Before
public void init() throws Exception {
    zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
        @Override
        public void process(WatchedEvent watchedEvent) {
            // 收到事件通知后的回调函数(用户的业务逻辑)
            System.out.println(watchedEvent.getType() + "--" + watchedEvent.getPath());
            // 再次启动监听
            try {
                List<String> children = zkClient.getChildren("/", true);
                for (String child : children) {
                    System.out.println(child);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

6.创建节点

// 创建子节点
@Test
public void create() throws Exception {
    // 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ;参数 4:节点的类型
    String nodeCreated = zkClient.create("/atguigu", "shuaige".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}

测试:在 hadoop102 的 zk 客户端上查看创建节点情况

[zk: localhost:2181(CONNECTED) 16] get -s /atguigu  shuaige

7.获取子节点并监听节点变化

// 获取子节点
@Test
public void getChildren() throws Exception {
    List<String> children = zkClient.getChildren("/", true);
    for (String child : children) {
        System.out.println(child);
    }
    // 延时阻塞
    Thread.sleep(Long.MAX_VALUE);
}

(1)在 IDEA 控制台上看到如下节点:

zookeeper
sanguo
atguigu

(2)在 hadoop102 的客户端上创建再创建一个节点/atguigu1,观察 IDEA 控制台

[zk: localhost:2181(CONNECTED) 3] create /atguigu1 "atguigu1"

(3)在 hadoop102 的客户端上删除节点/atguigu1,观察 IDEA 控制台

[zk: localhost:2181(CONNECTED) 4] delete /atguigu1

8.判断节点是否存在

// 判断 znode 是否存在
@Test
public void exist() throws Exception {
    Stat stat = zkClient.exists("/atguigu", false);
    System.out.println(stat == null ? "not exist" : "exist");
}

9.写数据原理

  • 写请求直接发送给leader节点

    1. 客户端将写请求发送leader节点,leader自己将数据写入
    2. leader节点通知逐步通知follower节点写数据
    3. 收到半数以上的follower节点回复已经写成功了,leader节点回复客户端写完成了
    4. leader节点继续通知其余节点写操作,并等待其回复
    5. 所有节点回复leader节点后,写操作完成
  • 写请求发送给follower节点

    1. 客户端将写请求发送给follower节点,follower节点将写请求转发给leader节点
    2. leader节点收到follower节点的写请求,将数据写入
    3. leader节点完成自己写操作后,通知follower节点写数据
    4. 收到半数以上的follower节点回复已经写成功了,leader节点回复发出写请求的follower节点数据已经写完了
    5. 收到客户端写请求的follower节点回复客户端写操作已完成。
    6. leader节点继续通知其余节点写操作,并等待其回复
    7. 所有节点回复leader节点后,写操作完成

九、服务器动态上下线

1.需求分析

某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知
到主节点服务器的上下线

2.服务器注册

(1)先在集群上创建/servers 节点

[zk: localhost:2181(CONNECTED) 10] create /servers "servers"
Created /servers

(2)在 Idea 中创建包名:com.atguigu.zkcase1

(3)服务器端向 Zookeeper 注册代码

import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
public class DistributeServer {
    private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    private static int sessionTimeout = 2000;
    private ZooKeeper zk = null;
    private String parentNode = "/servers";

    // 创建到 zk 的客户端连接
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
            }
        });
    }

    // 注册服务器
    public void registServer(String hostname) throws Exception {
        String create = zk.create(parentNode + "/server",
                hostname.getBytes(),
                Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println(hostname + " is online " + create);
    }

    // 业务功能
    public void business(String hostname) throws Exception {
        System.out.println(hostname + " is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {
// 1 获取 zk 连接
        DistributeServer server = new DistributeServer();
        server.getConnect();
// 2 利用 zk 连接注册服务器信息
        server.registServer(args[0]);
// 3 启动业务功能
        server.business(args[0]);
    }
}

3.客户端监听

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class DistributeClient {
    private static String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    private static int sessionTimeout = 2000;
    private ZooKeeper zk = null;
    private String parentNode = "/servers";

    // 创建到 zk 的客户端连接
    public void getConnect() throws IOException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 再次启动监听
                try {
                    getServerList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    // 获取服务器列表信息
    public void getServerList() throws Exception {
        // 1 获取服务器子节点信息,并且对父节点进行监听
        List<String> children = zk.getChildren(parentNode, true);
        // 2 存储服务器信息列表
        ArrayList<String> servers = new ArrayList<>();
        // 3 遍历所有节点,获取节点中的主机名称信息
        for (String child : children) {
            byte[] data = zk.getData(parentNode + "/" + child, false, null);
            servers.add(new String(data));
        }
        // 4 打印服务器列表信息
        System.out.println(servers);
    }

    // 业务功能
    public void business() throws Exception {
        System.out.println("client is working ...");
        Thread.sleep(Long.MAX_VALUE);
    }

    public static void main(String[] args) throws Exception {
        // 1 获取 zk 连接
        DistributeClient client = new DistributeClient();
        client.getConnect();
        // 2 获取 servers 的子节点信息,从中获取服务器信息列表
        client.getServerList();
        // 3 业务进程启动
        client.business();
    }
}

4.测试

1)在 Linux 命令行上操作增加减少服务器

(1)启动 DistributeClient 客户端

(2)在 hadoop102 上 zk 的客户端/servers 目录上创建临时带序号节点

[zk:localhost:2181(CONNECTED)1]create -e -s /servers/hadoop102 "hadoop102"
[zk:localhost:2181(CONNECTED)2]create -e -s /servers/hadoop103 "hadoop103"

(3)观察 Idea 控制台变化

[hadoop102, hadoop103]

(4)执行删除操作

[zk:localhost:2181(CONNECTED)8] delete /servers/hadoop1020000000000

(5)观察 Idea 控制台变化

[hadoop103]

2)在 Idea 上操作增加减少服务器

(1)启动 DistributeClient 客户端(如果已经启动过,不需要重启)

(2)启动 DistributeServer 服务

  1. 点击 Edit Configurations…

  2. 在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102

  3. 回 到 DistributeServer 的 main 方 法 , 右 键 , 在 弹 出 的 窗 口 中 点 击 Run “DistributeServer.main()”

  4. 观察 DistributeServer 控制台,提示 hadoop102 is working

  5. 观察 DistributeClient 控制台,提示 hadoop102 已经上线

十、分布式锁

什么叫做分布式锁呢?

比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。

1.需求分析

1)接收到请求后,在/locks节点下创建一个临时顺序节点

2)判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听

3)获取到锁,处理完业务后,delete节点释放锁,然后下面的节点将收到通知,重复第二步判断

2.代码实现

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class DistributedLock {
    // zookeeper server 列表
    private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
    // 超时时间
    private int sessionTimeout = 2000;
    private ZooKeeper zk;
    private String rootNode = "locks";
    private String subNode = "seq-";
    // 当前 client 等待的子节点
    private String waitPath;
    //ZooKeeper 连接
    private CountDownLatch connectLatch = new CountDownLatch(1);
    //ZooKeeper 节点等待
    private CountDownLatch waitLatch = new CountDownLatch(1);
    // 当前 client 创建的子节点
    private String currentNode;

    // 和 zk 服务建立连接,并创建根节点
    public DistributedLock() throws IOException, InterruptedException, KeeperException {
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                // 连接建立时, 打开 latch, 唤醒 wait 在该 latch 上的线程
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    connectLatch.countDown();
                }
                // 发生了 waitPath 的删除事件
                if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });
        // 等待连接建立
        connectLatch.await();
        //获取根节点状态
        Stat stat = zk.exists("/" + rootNode, false);
        //如果根节点不存在,则创建根节点,根节点类型为永久节点
        if (stat == null) {
            System.out.println("根节点不存在");
            zk.create("/" + rootNode, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    // 加锁方法
    public void zkLock() {
        try {
            //在根节点下创建临时顺序节点,返回值为创建的节点路径
            currentNode = zk.create("/" + rootNode + "/" + subNode,
                                    null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                    CreateMode.EPHEMERAL_SEQUENTIAL);
            // wait 一小会, 让结果更清晰一些
            Thread.sleep(10);
            // 注意, 没有必要监听"/locks"的子节点的变化情况
            List<String> childrenNodes = zk.getChildren("/" + rootNode, false);
            // 列表中只有一个子节点, 那肯定就是 currentNode , 说明            client 获得锁
            if (childrenNodes.size() == 1) {
                return;
            } else {
                //对根节点下的所有临时顺序节点进行从小到大排序
                Collections.sort(childrenNodes);
                //当前节点名称
                String thisNode = currentNode.substring(("/" +
                                                         rootNode + "/").length());
                //获取当前节点的位置
                int index = childrenNodes.indexOf(thisNode);
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0) {
                    // index == 0, 说明 thisNode 在列表中最小, 当前                    client 获得锁
                    return;
                } else {
                    // 获得排名比 currentNode 前 1 位的节点
                    this.waitPath = "/" + rootNode + "/" + childrenNodes.get(index - 1);
                    // 在 waitPath 上注册监听器, 当 waitPath 被删除时,                    zookeeper 会回调监听器的 process 方法
                    zk.getData(waitPath, true, new Stat());
                    //进入等待锁状态
                    waitLatch.await();
                    return;
                }
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 解锁方法
    public void zkUnlock() {
        try {
            zk.delete(this.currentNode, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

3.测试

(1)创建两个线程

import org.apache.zookeeper.KeeperException;
import java.io.IOException;

public class DistributedLockTest {
    public static void main(String[] args) throws InterruptedException, 
    IOException, KeeperException {
        // 创建分布式锁 1
        final DistributedLock lock1 = new DistributedLock();
        // 创建分布式锁 2
        final DistributedLock lock2 = new DistributedLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock1.zkLock();
                    System.out.println("线程 1 获取锁");
                    Thread.sleep(5 * 1000);
                    lock1.zkUnlock();
                    System.out.println("线程 1 释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取锁对象
                try {
                    lock2.zkLock();
                    System.out.println("线程 2 获取锁");
                    Thread.sleep(5 * 1000);
                    lock2.zkUnlock();
                    System.out.println("线程 2 释放锁");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

(2)观察控制台变化:

线程 1 获取锁
线程 1 释放锁
线程 2 获取锁
线程 2 释放锁

十一、算法基础

1.Paxos算法

解决什么问题:

Paxos算法:一种基于消息传递且具有高度容错特性的一致性算法。 Paxos算法解决的问题:就是如何快速正确的在一个分布式系统中对某个数据值达成一致,并且保证不论发生任何异常, 都不会破坏整个系统的一致性。

Paxos系统描述:

•在一个Paxos系统中,首先将所有节点划分为Proposer(提议者),Acceptor(接受者),和 Learner(学习者)。(注意:每个节点都可以身兼数职)。

一个完整的Paxos算法流程分为三个阶段:

  • Prepare准备阶段
    • Proposer向多个Acceptor发出Propose请求Promise(承诺)
    • Acceptor针对收到的Propose请求进行Promise(承诺)
  • Accept接受阶段
    • Proposer收到多数Acceptor承诺的Promise后,向Acceptor发出Propose请求
    • Acceptor针对收到的Propose请求进行Accept处理
  • Learn学习阶段:Proposer将形成的决议发送给所有Learners

Paxos算法流程:

  1. Prepare: Proposer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,这里无需携带提案内容,只携 带Proposal ID即可。
  2. Promise: Acceptor收到Propose请求后,做出“两个承诺,一个应答”。 ➢ 不再接受Proposal ID小于等于(注意:这里是<= )当前请求的Propose请求。 ➢ 不再接受Proposal ID小于(注意:这里是< )当前请求的Accept请求。 ➢ 不违背以前做出的承诺下,回复已经Accept过的提案中Proposal ID最大的那个提案的Value和Proposal ID,没有则 返回空值。
  3. Propose: Proposer收到多数Acceptor的Promise应答后,从应答中选择Proposal ID最大的提案的Value,作为本次要发起的 提案。如果所有应答的提案Value均为空值,则可以自己随意决定提案Value。然后携带当前Proposal ID,向所有Acceptor发送 Propose请求。
  4. Accept: Acceptor收到Propose请求后,在不违背自己之前做出的承诺下,接受并持久化当前Proposal ID和提案Value。
  5. Learn: Proposer收到多数Acceptor的Accept后,决议形成,将形成的决议发送给所有Learner

2.ZAB协议

2.1.什么是ZAB算法

ZAB协议借鉴了Paxos算法,是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。基于该协议,Zookeeper设计为只有一个leader可以发起提案,有多个follower节点接收并处理。

2.2.ZAB协议内容

ZAB协议包括两种基本模式:消息广播、崩溃恢复。

2.2.1.消息广播

  1. 客户端发起一个这操作

  2. leader服务端将客户端的请求转化为Proposal提案,同时为每个Proposal生成一个全局ID,即zxid。

  3. leader为每一个Follower分配一个单独的队列,然后将需要广播的Porposal依次放到队列中去,并根据FIFO策略进行消息发送。

  4. Follower接收到Proposal后,首先将其以事务日志的方式写入磁盘,写入成功后向leader反馈一个ACK响应消息。

  5. Leader接收到半数以上的Follower返回的ACK,即认为消息发送成功,可以发送commit消息。

  6. leader向所有follower广播commit消息,同时自身也会完成事务提交,Follower收到commit消息后,会将上一个事务提交。

  7. Zookeeper采用ZAB协议的核心是,只要有一台服务器提交了Porposal,就要确保所有的服务器最终都能正确提交Porposal。

消息广播的过程分为两个阶段:

1.Porposal广播,开启事务。

2.发送commit提交事务。

leader崩溃产生的两种情况:

  1. Porposal刚发送未收到半数follower的ACK时,崩溃。
  2. 未发送commit消息时,崩溃。

ZAB协议崩溃恢复需要满足以下两个条件:

  1. 确保丢弃已经被leader提出的,但没有被提交的proposal。
  2. 确保已经被leader提交的Proposal,必须最终被所有的Follower服务器提交。

2.2.2.崩溃恢复

即就是新leader选举:

  1. 新选举出来的leader不能包含未提交的proposal。即leader必须都是已经提交了proposal的follower服务器节点。
  2. 新选举出的leader节点中含有最大的zxid。这样做的好处是可以避免leader服务器检查proposal的提交和丢弃工作。

新leader的数据同步(ZAB数据同步):

  1. 完成leader选举之后,正式开始工作之前(接收事务请求,然后提出新的proposal),leader服务器首先会确认事务日志中所有的proposal是否已经被集群中过半的服务器commit。
  2. leader服务器需要确保所有的follower服务器都能接收到每一条事务的proposal,并且能将已提交的proposal应用到内存数据中。等follower将所有尚未同步的事务proposal都从leader服务器上同步过,并且应用到内存数据以后,leader才会把follower加入到真正可用的follower列表中。

3.CAP理论

CAP理论是分布式系统的一个指标。

  • 一致性 C:Consistency

分布式环境中,数据在多个副本之间是否能够保持数据一致的特性。

  • 可用性 A:Available

系统提供的服务必须一直处于可用状态,对于用户的请求总能在有限时间内返回结果。

  • 分区容错性 P:Partition Tolerance

分布式系统在遇到任何网络分区故障的时候,任然需要保证对外提供满足一致性和可用性的服务。除非所有网络环境全部故障。

#这是一个基本需求,最多只能同时满足两个,因为P是必须的,所以往往选择就在CP或者AP中。

Zookeeper保证的是CP

  1. Zookeeper不能保证每次服务请求的可用性。(注:极端情况下,Zookeeper可能会丢弃一些请求,消费者需要重新请求才能得到结果)。
  2. 进行leader选举的时候集群都是不可用的。(没有proposal提出者)
posted @ 2022-09-26 09:56  howard4  阅读(73)  评论(0编辑  收藏  举报