目录
一、概念
Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。 Zookeeper=文件系统+通知机制
Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应,从而实现集群中类似Master/Slave管理模式
提供的服务包括:分布式消息同步和协调机制、服务器节点动态上下线、统一配置管理、负载均衡、集群管理等。
二、安装部署
1、安装
(1)安装jdk 解压zookeeper安装包
(2)配置修改,将/zookeeper-3.4.10/conf这个路径下的zoo_sample.cfg修改为zoo.cfg;
dataDir=/opt/module/zookeeper-3.4.10/data/zkData
server.1=hdp-1:2888:3888
server.2=hdp-2:2888:3888
server.3=hdp-3:2888:3888
#配置参数解读Server.A=B:C:D。
#A是一个数字,表示这个是第几号服务器;
#B是这个服务器的ip地址;
#C是这个服务器与集群中的Leader服务器交换信息的端口;
#D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是
#用来执行选举时服务器相互通信的端口。
#集群模式下配置一个文件myid,这个文件在dataDir目录下,这个文件里面有一个数据就是A的值,Zookeeper#启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。
(3)每个服务器dataDir目录下创建一个myid的文件在文件中添加与server对应的编号:如2
2、实战操作
(1)启动zookeeper
[xin@hdp-1 zookeeper-3.4.10]$ bin/zkServer.sh start
(2)查看状态:
[root@hdp-2 bin]# ./zkServer.sh status
JMX enabled by default
Using config: /root/apps/zookeeper-3.4.6/bin/../conf/zoo.cfg
Mode: leader
(4)启动客户端:
[xin@hdp-1 zookeeper-3.4.10]$ bin/zkCli.sh
(5)退出客户端:
[zk: localhost:2181(CONNECTED) 0] quit
(6)停止zookeeper
[xin@hdp-1 zookeeper-3.4.10]$ bin/zkServer.sh stop
(7)脚本实现集群启动/停止zookeeper服务
#! /bin/bash
for host in hdp-1 hdp-2 hdp-3
do
echo "${host}:${1}ing...."
ssh $host "source /etc/profile;/root/apps/zookeeper-3.4.6/bin/zkServer.sh $1"
done
三、Zookeeper理论篇之zoo.cfg
解读zoo.cfg 文件中参数含义
1)tickTime:通信心跳数,Zookeeper服务器心跳时间,单位毫秒。Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
2)initLimit:LF初始通信时限,集群中的follower跟随者服务器(F)与leader领导者服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。投票选举新leader的初始化时间,Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许F在initLimit时间内完成这个工作。
3)syncLimit:LF同步通信时限,集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,
Leader认为Follwer死掉,从服务器列表中删除Follwer。在运行过程中,Leader负责与ZK集群中所有机器进行通信,例如通过一些心跳检测机制,来检测机器的存活状态。如果L发出心跳包在syncLimit之后,还没有从F那收到响应,那么就认为这个F已经不在线了。
4)dataDir:数据文件目录+数据持久化路径,保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。
5)clientPort:客户端连接端口,监听客户端连接的端口
四、Zookeeper理论篇之数据结构
组员关系:
理解Zookeeper的一种方法是将其看作一个具有高可用性特征的文件系统。这个文件系统中没有文件和目录,而是统一使用“节点”(node)的概念,成为znode。znode既可以作为保存数据的容器(如同文件),也可以做为保存其他node的容器(如同目录)。所有的znode构成了一个层次化的命名空间,树形结构。Zookeeprt被用来实现协调服务(这类服务通常使用小数据文件),所以一个znode能存储的数据限制在1M以内。
1、节点类型
1)Znode有两种类型:
短暂(ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
持久(persistent):客户端和服务器端断开连接后,创建的节点不删除
2)Znode有四种形式的目录节点(默认是persistent )
(1)持久化目录节点(PERSISTENT)
客户端与zookeeper断开连接后,该节点依旧存在
(2)持久化顺序编号目录节点(PERSISTENT_SEQUENTIAL)
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
(3)临时目录节点(EPHEMERAL)
客户端与zookeeper断开连接后,该节点被删除
(4)临时顺序编号目录节点(EPHEMERAL_SEQUENTIAL)
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
2、特点
1)Zookeeper:一个领导者(leader),多个跟随者(follower)组成的集群。
2)Leader负责发起决议投票,更新系统状态,Follower接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
3)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务,所以zookeeper适合装在奇数台机器上。
4)全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的。
5)更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行。
6)原子广播:半数以上跟随者持久化成功,leader才会提交更新,数据更新原子性,一次数据更新要么成功,要么失败。
7)实时性,在一定时间范围内,client能读到最新数据。
3、选举机制
假设有五台服务器组成的zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么。
(1)服务器1启动,此时只有一台服务器,它发出去的请求没有任何响应,所以它是LOOKING状态。
(2)服务器2启动,与服务器1进行通信,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3),所以服务器1、2还是继续保持LOOKING状态。
(3)服务器3启动,根据前面的理论分析,服务器3成为服务器1、2、3中的老大,而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的leader。
(4)服务器4启动,根据前面的分析,理论上服务器4应该是服务器1、2、3、4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了。服务器5启动,同4一样当小弟。
4、stat结构体
1)czxid- 引起这个znode创建的zxid,创建节点的事务的zxid(ZooKeeper Transaction Id)
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
2)ctime - znode被创建的毫秒数(从1970年开始)
3)mzxid - znode最后更新的zxid
4)mtime - znode最后修改的毫秒数(从1970年开始)
5)pZxid-znode最后更新的子节点zxid
6)cversion - znode子节点变化号,znode子节点修改次数
7)dataversion - znode数据变化号
8)aclVersion - znode访问控制列表的变化号
9)ephemeralOwner- 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
10)dataLength- znode的数据长度
11)numChildren - znode子节点数量
五、Zookeeper实战
1、客户端命令
命令基本语法 | 功能描述 |
help | 显示所有操作命令 |
ls path [watch] | 使用 ls 命令来查看当前znode中所包含的内容 |
ls2 path [watch] | 查看当前节点数据并能看到更新次数等数据 |
create | 普通创建 -s 含有序列 -e 临时(重启或者超时消失) |
get path [watch] | 获得节点的值 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
rmr | 递归删除节点 |
详细操作:
[root@hdp-2 bin]# ./zkCli.sh #启动
[zk: localhost:2181(CONNECTED) 0] ls #查看当前znode中所包含的内容
[zk: localhost:2181(CONNECTED) 1] ls / #查看当前节点数据并能看到更新次数等数据
#创建普通节点
[zk: localhost:2181(CONNECTED) 2] create /app1 "xintest"
Created /app1
[zk: localhost:2181(CONNECTED) 4] create /app1/server101 "192.168.1.101"
Created /app1/server101
#获得节点的值
[zk: localhost:2181(CONNECTED) 5] get /app1
"xintest"
#创建短暂节点 在当前客户端是能查看到的,退出客户端即被删除
[zk: localhost:2181(CONNECTED) 9] create -e /app-emphemeral 8888
#创建带序号的节点 如果原节点下有1个节点,则再排序时从1开始,以此类推
[zk: localhost:2181(CONNECTED) 13] create -s /app2/aa 888
#修改节点数据值
[zk: localhost:2181(CONNECTED) 2] set /app1 999
#节点的值变化监听
# 1、在104主机上注册监听/app1节点数据变化
[zk: localhost:2181(CONNECTED) 26] get /app1 watch
# 2、在103主机上修改/app1节点的数据
[zk: localhost:2181(CONNECTED) 5] set /app1 777
# 3、观察104主机收到数据变化的监听
WATCHER:: WatchedEvent state:SyncConnected type:NodeDataChanged path:/app1
#节点的子节点变化监听(路径变化)
# 1、在104主机上注册监听/app1节点的子节点变化
[zk: localhost:2181(CONNECTED) 1] ls /app1 watch
[aa0000000001, server101]
# 2、在103主机/app1节点上创建子节点
[zk: localhost:2181(CONNECTED) 6] create /app1/bb 666
Created /app1/bb
# 3、观察104主机收到子节点变化的监听
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/app1
#删除节点
[zk: localhost:2181(CONNECTED) 4] delete /app1/bb
#递归删除节点
[zk: localhost:2181(CONNECTED) 7] rmr /app2
#查看节点状态
[zk: localhost:2181(CONNECTED) 12] stat /app1
六、JAVA API
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>zookeeper</artifactId>
<version>3.3.1</version>
</dependency>
ZkClient.java
package com.xin;
import java.util.ArrayList;
import java.util.List;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
public class ZkClient {
static ZooKeeper zk = null;
public static void main(String[] args) throws Exception {
ZkClient zkclient = new ZkClient();
// 1、创建连接
zkclient.getConnection();
// 2、监听节点变化
zkclient.getClient();
// 3、业务实现
zkclient.business();
}
// 1、创建连接
public void getConnection() throws Exception {
zk = new ZooKeeper("hdp-1:2181,hdp-2:2181,hdp-3:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 收到事件通知后的回调函数(用户的业务逻辑)
if (KeeperState.SyncConnected.equals(event.getState())
&& EventType.NodeChildrenChanged.equals(event.getType())) {
System.out.println("-------------服务器有变化--------------------------");
} else if (KeeperState.SyncConnected.equals(event.getState())
&& EventType.NodeDataChanged.equals(event.getType())) {
System.out.println("-------------服务器数据有变化--------------------------");
}
// 再次启动监听
try {
getClient();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 2、监听节点变化
public void getClient() throws Exception {
List<String> children = zk.getChildren("/servers", true);
ArrayList<String> servers = new ArrayList();
for (String string : children) {
byte[] data = zk.getData("/servers/" + string, true, null);
servers.add(new String(data));
}
System.out.println(servers);
}
// 3、业务具体实现
public void business() throws Exception {
System.out.println("下线监听已经完成!!");
Thread.sleep(Long.MAX_VALUE);
zk.close();
}
}
ZkClientTwo.java
package com.xin;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.util.List;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.junit.Before;
import org.junit.Test;
public class ZkClientTwo {
public class ZkClient {
//连接zk的地址以及端口号
private String connectString="hdp-1:2181,hdp-2:2181,hdp-3:2181";
//连接zk超时时间,单位毫秒
private int sessionTimeout =2000;
private ZooKeeper zooKeeper=null;
//1、创建客户端
@Before
public void initZk() throws Exception {
zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 收到事件通知后的回调函数(用户的业务逻辑)
//当zookeeper有数据变化才会被触发
//除new zookeeper之外有其他任意操作都会启动回调函数,且先执行回调函数。如果其他操作有监听机制,且触发机制后会再次调用回调函数;
System.out.println(event.getType()+event.getPath());
//state 事件更改状态 type事件类型
if(KeeperState.SyncConnected.equals(event.getState()) &&EventType.NodeChildrenChanged.equals(event.getType())) {
System.out.println("--------------------------");
}
//一直执行此监听任务
// try {
// Stat exists = zooKeeper.exists("/xiyouji", true);
// } catch (Exception e) {
// e.printStackTrace();
// }
}
});
// zooKeeper.close();
}
@Test
//2、创建子节点
public void createNode() throws Exception {
//参数1:创建子节点的路径 参数2:子节点的数据 3、创建子节点后节点带有的权限 4、节点类型
String create = zooKeeper.create("/shixun", "a.avi".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("++++++++++"+create);
}
//删除节点
@Test
public void deleteNode() throws Exception{
zooKeeper.delete("/shixun", -1);
}
//给节点赋值
@Test
public void setData() throws Exception{
Stat setData = zooKeeper.setData("/xiyouji", "123".getBytes(), -1); //-1表示修改之前版本
}
//获取节点内容
@Test
public void getData() throws Exception{
byte[] data = zooKeeper.getData("/xiyou", true, null); //null -->0表示改变之前,1改变之后
System.out.println(new String(data,"utf-8"));
Thread.sleep(Long.MAX_VALUE);
}
//获取该位置的子节点数目
@Test
public void getChild() throws Exception{
List<String> children = zooKeeper.getChildren("/xiyouji", false); //false表示不监听
for (String string : children) {
System.out.println(string);
}
}
//判断节点是否存在
@Test
public void isExist() throws Exception {
Stat exists = zooKeeper.exists("/xiyouji", true);
System.out.println(exists ==null ? "not exist":"is exist");
Thread.sleep(Long.MAX_VALUE);
}
}
}
ZkSever.java
package com.xin;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
public class ZkSever {
static ZooKeeper zk=null;
public static void main(String[] args) throws Exception {
ZkSever zkSever = new ZkSever();
//1、创建连接
zkSever.getConnection();
//2、注册
zkSever.regist("5555");
//3、业务具体实现
zkSever.business();
}
//1、创建连接
public void getConnection() throws Exception {
zk = new ZooKeeper("hdp-1:2181,hdp-2:2181,hdp-3:2181", 2000, new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
}
//2、注册
public void regist(String hostname) throws Exception {
String create = zk.create("/servers"+"/sever", hostname.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(hostname+" is online "+create);
}
//3、业务具体实现
public void business() throws Exception {
System.out.println("上线监听已经完成!!");
Thread.sleep(Long.MAX_VALUE);
zk.close();
}
}
七、分布式锁
1、悲观锁,又称为悲观并发控制(Pessimistic Concurrency Control,PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权利更新这份数据。一般我们认为,在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景,在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制的问题。
2、乐观锁,又称为乐观并发控制(Optimistic Concurrency Control,OCC),也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。悲观锁假定不同事务之间的处理一定会出现互相干扰,从而需要在一个事务从头到尾的过程中都对数据进行加锁处理。而乐观锁则不会对多个进程产生影响。因此在事务处理的绝大部分时间里不需要进行加锁处理。乐观锁机制中,在更新请求提交前,每个事务都会首先检查当前事务读取数据之后,是否会有其他事务对此数据进行了修改。如果其他事务有更新,则正在提交的数据必须要回滚。乐观锁通常适用于使用在数据并发不大,事务冲突较少的应用场景中。
乐观锁有三个阶段,分别是数据读取、写入校验和数据写入,其中写入校验是整个乐观锁控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。