Zookeeper学习笔记


文章学习笔记内容来源:拉勾教育大数据开发高薪训练营。

记录整理一下Zookeeper的一些的学习笔记以方便后续复习。

Zookeeper组成

Zookeeper 特点

  • Zookeeper本质上是一个分布式的小文件存储系统,类型Linux文件目录系统,每个节点都可以存储数据。
  • ZooKeeper提供给客户端监控存储在zk内部数据的功能,采用临时节点和Watcher实现。
  • Zookeeper集群中只有一个leader多个follower。
  • Leader负责进行投票的发起和决议,更新系统状态,负责集群的数据写请求,事务请求[create,setdata,delete等]的唯一处理者。
  • Follower只能负责客户端的读请求,参与leader的选举。写请求由leader负责,follow接收到写请求会转发给leader处理。
  • 数据更新时全部按照顺序更新,写数据是原子性操作。
  • 集群中的数据在每个节点保持一致,集群中只要有半数节点存活,集群就能就能正常工作。
  • 集群中可以设置Observer(观察者角色):能处理读请求,更新写数据转发给leader处理,不参与集群中的投票。作用就是处理客户端的读请求,增加集群的并发读的能力。

Zookeeper集群搭建

下载Zookeeper包,这里使用的是zookeeper-3.4.14.tar.gz,上传至centos系统,并解压。
解压 tar -zxvf zookeeper-3.4.14.tar.gz -C /user/local/software/
对解压后的目录进行修改:修改配置文件创建data与log目录
#创建zk存储数据目录
mkdir -p /user/local/software/zookeeper-3.4.14/data
#创建zk日志文件目录
mkdir -p /user/local/software/zookeeper-3.4.14/data/logs
#修改zk配置文件
cd /user/local/software/zookeeper-3.4.14/conf
#文件改名
mv zoo_sample.cfg zoo.cfg
编辑修改 vi zoo.cfg
#更新datadir
dataDir=/user/local/software/zookeeper-3.4.14/data
#增加logdir
dataLogDir=/user/local/software/zookeeper-3.4.14/data/logs
#增加集群配置
##server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
server.1=192.168.56.121:2888:3888
server.2=192.168.56.122:2888:3888
server.3=192.168.56.123:2888:3888
#打开注释
#ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时
autopurge.purgeInterval=1

添加myid配置,zookeeper节点ID标识,唯一
在zookeeper的 data 目录下创建一个 myid 文件,内容为1,这个文件就是记录每个服务器的ID
cd /user/local/software/zookeeper-3.4.14/data
echo 1 > myid

把刚刚修改的zookeeper解压包,复制到另外2台机器上,并修改myid中的值,依次修改为2和3
修改配置好了之后,启动集群安装目录bin下面:./zkServer.sh start,关闭./zkServer.sh stop

Zookeeper数据结构与监听机制

ZNode 的类型

zookeeper中的数据结构类似于linux的文件系统目录,每一个节点可以存放值
在这里插入图片描述

  • 持久节点:节点在zookeeper上面创建后就一直存在,除非被主动删除。
  • 持久顺序节点:和持久节点一样。但是创建顺序节点时,会在节点名称后面加上一个数字后缀,来标识顺序
  • 临时节点:会被自动的删除到,和客户端连接zookeeper的session生命周期有关。客户端会话结束,该节点会自动被zookeeper删除。与持久性节点不同的是,临时节点不能创建子节点
  • 临时顺序节点:有临时节点的特性,创建字节时,会在节点名称后面加上一个数字后缀,来标识顺序
  • 事务ID:zookeeper中对数据节点创建、删除、更新等数据节点操作,zookeeper都会维护一个ZXID,通常是一个 64 位的数字。每一个 ZXID 对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。

节点信息

get path 查看节点信息

cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后一次被修改的时间。
pZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子
节点内容变更不会更新。
cversion 表示子节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据长度。
numChildren 表示直系子节点数。

Watcher 机制

Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能(推(Push)模式/拉取(Pull)模式)
Zookeeper允许客户端向服务端注册一个watcher监听,当服务端的一些指定事件触发了这个watcher,那么zookeeper就会向指定客户端发送一个事件通知来实现分布式的通知功能。
在这里插入图片描述

Zookeeper常用命令

通过ZkClient进行zookeeper客户端进行操作

./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port(2181) 连接指定的服务器

help 查看帮助

stat path [watch]
set path data [version]
ls path [watch]
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]
create [-s] [-e] path data acl
addauth scheme auth
quit
getAcl path
close
connect host:port

create命令,可以创建一个Zookeeper节点

create [-s][-e] path data
其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点

ls命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点

ls path
path表示的是指定数据节点的节点路径

get命令可以获取Zookeeper指定节点的数据内容和属性信息

get path
path表示的是指定数据节点的节点路径

set命令,可以更新指定节点的数据内容

set path data
path表示的是指定数据节点的节点路径
data表示要存放的数据

delete命令可以删除Zookeeper上的指定节点

delete path
path表示的是指定数据节点的节点路径

rmr 递归删除节点

rmr path
path表示的是指定数据节点的节点路径

Zookeeper内部原理

Leader选举

选举机制:过半机制

zookeeper采用的是过半机制:如集群中有3台设备,有2台集群同意就可以产生leader,允许集群宕机一台集群。

现在有5个节点构成的Zookeeper集群
在这里插入图片描述
集群首次启动:首次启动,集群上面还没有数据

  1. 节点1启动,此时集群中只有一台机器存活,发出的选择请求没有任何响应,状态为LOOKING
  2. 节点2启动,发送选举报文与节点1交换选举结果,节点2的id值大,节点2赢取选举,但是此时集群中只有2台集群存活还没有达到过半机制,节点2也是LOOKING状态
  3. 节点3启动,发送报文与节点2、节点1进行选举,节点3的id最大,赢取选举,此时集群中已经有一半机器存储,有3台机器选举节点3,自己也会选择自己,所以节点3成为leader,节点1、2成为follower.
  4. 节点4启动,与节点1、2、3进行选举通信,发现集群中已经产生了leader,此时节点4成为follower
  5. 节点5启动,同节点4一样成为follower。

集群非首次启动

每个节点在选举时都会参考自身节点的zxid值(事务ID,优先选择zxid值大的节点称为Leader!

ZAB一致性协议

ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复和原子广播协议

客户端所以对数据节点的事务操作都是由Leader进行操作,先leader节点进行处理,然后由Leader复制到follower中。ZAB会将服务器数据状态变更以及事务Proposal的形式广播到所以的follower中,ZAB协议能够保证了事务操作的一个全局的变更序号(ZXID)

广播消息

  • 对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal(提议),将其发送给所有 follower ,如果收到超过半数反馈ACK,则执行 Commit 操作(先提交自己,再发送 Commit 给所有 follower
  • leader接收到client请求后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一ID(事务ID也叫ZXID),ZAB协议要求保证事务的顺序,因此必须将每一个事务按照ZXID进行先排序后处理
  • Zookeeper集群为了保证任务事务操作能够有序的执行,所以的事务只能是Leader处理,follower接收到,也要转发到leader进行处理
  • zookeeper提供的是最终一致性。zookeeper所有节点接收写请求之后可以在一定时间内保证所有节点都能看到该条数据。
    在这里插入图片描述

Leader宕机

  • Leader宕机后,zookeeper集群无法正常工作,ZAB协议提供了一个高效且可靠的leader选举算法。如果集群中超过一半的机器宕机后,整个集群集群也会无法正常工作,Zookeeper采用的过半机制。
  • Leader宕机后,ZAB协议确保那些已经在leader提交的事务会被最终提交,确保丢弃那些只在leader提出/复制,但是没有提交的事务。
    ZAB协议:保证选举出的新Leader拥有集群中所有节点最大编号(ZXID)的事务。

Zookeeper应用实践

Zookeeper的两大特性

  1. 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点,列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
  2. 对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被,自动删除

zk实现分布式锁

利用Zookeeper的两大特性可以用于来实现分布式,能创建成功临时节点,则表示获取到锁。关闭会话,Zookeeper会自动删除创建的临时节点,则表示释放锁,在利用watcher机制就可以感知到Zookeeper的节点信息变化。
这里采用临时顺序节点进行实现实现分布式锁,没有采用临时节点实现。

//1.去zk创建临时序列节点,并获取到序号
//2.判断自己创建节点序号是否是当前最小节点如果是则获取锁进行相关操作
// 执行相关操作 最后要释放锁
//3不是最小节点,当前线程需要等待 等待你的前面一个序号的节点
// 被删除 然后再次判断自己是否是最小节点
public class DisClient {
    // 前一个节点
	private String beforeNodePath;
	private String currentPath;
	CountDownLatch countDownLatch = null;
	private static ZkClient zkClient = new ZkClient("linux121:2181,linux122:2181,linux123:2181");
	
	public DisClient() {
		// 初始化zk的 distrilock节点 手工创建
	}
	
	// 释放锁
	public void deleteLock() {
		if(zkClient != null) {
			zkClient.delete(currentPath);
		}
	}
	
	// 把抢锁过程分为两部分
	// 1部分创建节点,比较序号
	// 2等待锁
	
	// 完整获取锁方法
	public void getDisLock() throws InterruptedException {
		// 首先钓鱼tryGetLock
		if(tryGetLock()) {
			// 说明获取到锁
			System.out.println(Thread.currentThread().getName() + ":获取到了锁");
		} else {
			System.out.println(Thread.currentThread().getName() + ":没有获取到锁,获取锁失败");
		    waitForLock();
		    // 递归获取锁
		    getDisLock();
		}
	}
	
	// 尝试获取锁
	//创建临时顺序节点    /distrilock/序号
	public boolean tryGetLock() {
		if(currentPath == null || "".equals(currentPath)) {
			currentPath = zkClient.createEphemeralSequential("/distrilock/", "lock");
		}
	    // 获取所有子节点
		List<String> childs = zkClient.getChildren("/distrilock");
	     
		Collections.sort(childs);
		
		
		String minNode = childs.get(0);
		//System.out.println("=========" + childs.size() + "========" + currentPath + "=====" + minNode);
		// 判断自己创建节点是否与最小节点是否一致
		if(currentPath.equals("/distrilock/" + minNode)) {
			return true;
		} else {
			// 说明不是自己创建的,要监控自己创建当前序号的前一个的节点
			int ii = Collections.binarySearch(childs, currentPath.substring("/distrilock/".length()));
			// 前一个(lastnodeChild是不包括父节点的)
			String lastNodeChild = childs.get(ii  -1);
			beforeNodePath  = "/distrilock/" + lastNodeChild;
		}
		return false;
	}
	
	
	// 等待之前节点释放锁,如何判断锁被释放判断,需要唤醒线程继续尝试tryGetLock
	public void waitForLock() throws InterruptedException {
		
		// 开始线程等待  线程同步计数器
		countDownLatch = new CountDownLatch(1);
		
		// 准备一个监听器
		IZkDataListener dataListener = new IZkDataListener() {
			// 上一个节点被删除
			@Override
			public void handleDataDeleted(String dataPath) throws Exception {
				// 提醒当前线程在次获取锁
				countDownLatch.countDown(); // 执行一次减一
			}
			
			@Override
			public void handleDataChange(String dataPath, Object data) throws Exception {
				
			}
		};
		// 监控前一个节点
		zkClient.subscribeDataChanges(beforeNodePath, dataListener);
		
		// 在监听的通知没有来之前线程应该是等待状态,先判断一次上一个节点是否还存在
		if(zkClient.exists(beforeNodePath)) {
			countDownLatch.await(); // 阻塞,CountDownLatch变成0
		} else {
			countDownLatch.countDown(); // 执行一次减一
		}
		
		zkClient.unsubscribeDataChanges(beforeNodePath, dataListener);
	}
}
// zk实现一个分布式锁
public class DisLockTest {
	private static int num = 0;

	public static void add() {
		num ++;
	}
	public static void main(String[] args) {
		CountDownLatch countDownLatch = new CountDownLatch(10);
		// 使用10个线程模拟分布式环境
		for (int i = 0; i < 10; i++) {
			new Thread(() -> {
		      for(int j = 0; j < 100; j ++) {
		    	  DisClient client = new DisClient();
	              try {
					   client.getDisLock();
					   add();
					   //TimeUnit.SECONDS.sleep(1);
				  } catch (Exception e) {
					 e.printStackTrace();
				  } finally {
					client.deleteLock();
				  } 
		      }
		      countDownLatch.countDown();
			}).start();
		}
		while (true) {
            if (countDownLatch.getCount() == 0) {
                System.out.println(num);
                break;
            }
		}
	}
}
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页