简单介绍
zookeeper是Apache Hadoop项目下的一个子项目,是一个树形目录服务。中文翻译为动物管理员,用来管理Hadoop(大象),Hive(蜜蜂),pig(小猪)的管理员,简称zk,它是一个分布式的、开源的分布式应用程序的协调服务。
它主要提供三个功能:
1、配置管理
有多个服务都需要连接数据库,每一个服务连接数据库的地址,用户名、密码都是一样的,这时候可以抽出来一个配置中心,将用户名,密码,服务器地址放在配置中心里面,zookeeper可以作为这个配置中心
2、分布式锁
当程序A访问数据的时候,为了避免其他程序的影响,可以加一把锁,如果程序B和A不是在一个电脑上,这时程序B要想访问数据,A的锁不能阻止B来访问数据,为了避免这种现象的发生,可以使用分布式锁,意思是A和其他程序都去“分布式锁”那里去要锁,如果A访问数据拿到了锁,B在访问数据时去“分布式锁“那里要锁,则锁会没有。zookeeper可以充当分布式锁的作用。
3、集群管理
即服务间调用。和nacos一样
zookeeper的样子:
单体安装
1、安装好zookeeper之后进行解压,目录自己定义即可
2、在安装好的目录下面使用mkdir命令建两个文件夹,data和logs
3、在conf目录下面找到配置文件zoo.cfg,然后复制一份为zoo1.cfg,命令为cp zoo.cfg zoo1.cfg
4、复制好之后修改该配置文件,将data和logs加入到该文件内,输入命令: vi zoo1.cfg之后添加如下:
5、在data目录下面创建 myid 文件使用命令 echo “1” > myid
6、配置环境变量,可以在任意目录启动zk,也可以不配。找到.bash_profile文件,添加zookeeper配置文件,(在用户目录下面,cd~),输入命令 :vi .bash_profile
7、使配置文件生效,输入命令: source .bash_profile
8、关闭防火墙:
输入命令:sudo systemctl stop firewalld.service
9、启动并测试:
启动命令: zkServer.sh start
查看zk状态: zkServer.sh status
10、关闭zookeeper
输入命令:zkServer.sh stop
集群搭建
搭建方式和单体搭建是一样的,需要安装三次,但是端口不一样即可,这里搭建的是伪集群。
1、设置zk的myid
2881端口的zk,myid为1
2882端口的zk,myid为2
2883端口的zk,myid为3
2、设置每一个zk的配置文件
在每一个zoo.cfg最下面加上同样的代码
server.1=192.168.162.128:2881:3881
server.2=192.168.162.128:2882:3882
server.3=192.168.162.128:2883:3883
如果是真正的集群的话,则除了服务器地址是不一样的,其他全部都是一样的
3、启动
进入到bin目录下面如果启动命令为 ./zkServer.sh start, 但是报权限不够的错误,则需要提权,
输入命令:chmod a+xwr zkServer.sh 即可。
zk的选举机制
集群搭建的时候需要选取一个zk作为leader,选取leader的方法如下:
leader是5个集群中(假设该集群有5台服务器)的领导者,它说了算。假设一个集群一共有5个服务器zk,每一个zk有自己的id,id编号越大表示在选择算法中的权重就越大。在leader选举的过程中,如果某台zk获得了超过半数的选票,则此台zk可以成为leader了。
假设该集群一共有5个zk,1起来之后投了自己一票,没有过半(才5分之1),不能成为leader,2起来之后 1和2都投了2一票,也没有过半,同理;3起来之后,三个都投了3一票,则3成为leader,之后就不用再继续投票了。
事务请求表示增删改的操作,非事务请求表示查询的操作。
调度的意思就是给各个sever发通知进行同步节点的数据。
observer和follower唯一的区别就是不具有投票的功能,它的存在意义是为了给followe分担压力
遇到的问题
集群启动的时候报如下错误:
ZooKeeper JMX enabled by defaultUsing config: /usr/soft/zookeeper/apache-zookeeper-3.5.8-bin/bin/…/conf/zoo.cfgStarting zookeeper … already running as process 7145.
解决方法:之前搭建的单体的时候配置了环境变量,所以需要将 用户目录下面的 .bash_profile 中配置的zk目录删除掉
然后进入新建zk的bin文件夹下面,输入命令chmod a+xwr zkServer.sh 进行提权,然后启动即可: ./zkServer.sh start
常见命令
启动zookeeper:
cd到bin目录下面 输入命令:zkServer.sh start
查看zookeeper状态:
zkServer.sh status
关闭zookeeper:
zkServer.sh stop
注意启动客户端之前要先启动服务端,并且要关闭防火墙。
1、当启动完客户端之后输入Ls / 命令可以查看子节点
2、创建节点的方式:
create /节点的名字 节点的数据
这里也可以不写节点的数据,后面再进行set写数据也可以。
3、获取子节点的数据方式:
get /子节点的名字
4、给子节点设置数据:
Set /子节点的名字 数据
5、删除子节点的命令:
Delete /子节点的名字
6、删除全部子节点,如果一个节点下面有多个字节点,全部删除的命令:
deleteall /节点
7、如果命令记不清了,可以输入help来查询命令
创建持久化节点命令:
create -s /app1
创建临时节点命令:
Create -es /app2
查看节点详细信息命令:
Ls -s /app3
Curator使用
curator就是zk的java客户端
1、节点的创建、查询、修改、删除
public class CuratorTest {
private CuratorFramework curatorFramework;
/**
* 建立连接
*/
@Before
public void testConnect() {
//连接的策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
/*
* Create a new client
*
* @param connectString 连接的地址
* @param sessionTimeoutMs session超时时间
* @param connectionTimeoutMs 连接超时时间
* @param retryPolicy 重试连接的策略
* @return client
*/
//第一种方式
//curatorFramework = CuratorFrameworkFactory.newClient("192.168.162.128:2181", 60 * 1000, 15 * 1000, retryPolicy);
开启连接
//curatorFramework.start();
//------------------------------------------------------------------------------------------------------------------------------
//第二种方式;可以添加默认的命名空间,以后节点都会放到该命名空间的节点下面
curatorFramework = CuratorFrameworkFactory.builder().connectString("192.168.162.128:2181").sessionTimeoutMs(60 * 1000).connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy).namespace("ming").build();
//开启连接
curatorFramework.start();
}
/**
* @Description 测试创建
* @Date 2021/6/5 16:41
* @Param []
* @Return void
*/
@Test
public void testCreate() throws Exception {
//1、基本创建
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据进行存储
String s = curatorFramework.create().forPath("/app1");
System.out.println(s);
}
@Test
public void testCreate2() throws Exception {
//1、创建节点,带有数据的
//如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据进行存储
String s = curatorFramework.create().forPath("/app2", "hahaha".getBytes());
System.out.println(s);
}
@Test
public void testCreate3() throws Exception {
//1、设置节点的类型
//默认的类型是 持久性;下面的意思是当前会话(当前方法)一旦结束,当前创建的节点就会被删除掉。
String s = curatorFramework.create().withMode(CreateMode.EPHEMERAL).forPath("/app3");
System.out.println(s);
//执行下面的方法,可以让方法不结束
while (true) {
}
}
@Test
public void testCreate4() throws Exception {
//1、创建多级节点 /app4/p1
//creatingParentContainersIfNeeded:意思是如果当前节点需要父节点的话,可以进行创建父节点
String s = curatorFramework.create().creatingParentContainersIfNeeded().forPath("/app4/p1");
System.out.println(s);
}
//==========================================查询数据=======================================
/**
* @Description 查询数据
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void testGet1() throws Exception {
byte[] bytes = curatorFramework.getData().forPath("/app1");
System.out.println(new String(bytes));
}
/**
* @Description 查询子节点
* @author Liu Yiming
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void testGet2() throws Exception {
List<String> list = curatorFramework.getChildren().forPath("/app4");
for (String s : list) {
System.out.println(s);
}
}
/**
* @Description 获取节点状态信息
* 等同于 客户端中的 ls -s /
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void testGet3() throws Exception {
//将状态信息放到 Stat这个容器里面。
Stat stat = new Stat();
curatorFramework.getData().storingStatIn(stat).forPath("/app1");
System.out.println(stat);
}
//======修改操作====================================================================================
/**
* @Description 基本的修改操作
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void set() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
curatorFramework.setData().forPath("/app1", "hahahahah".getBytes());
}
/**
* @Description 根据版本来更新数据
* 多个javaApi来对同一个客户端进行操作的话,可能会覆盖数据,所以可以指定版本来进行修改
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void setWithVersion() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
Stat status = new Stat();
curatorFramework.getData().storingStatIn(status).forPath("/app1");
int version = status.getVersion();
curatorFramework.setData().withVersion(version).forPath("/app1", "lalalalala".getBytes());
}
//====删除操作======================================================================================================
/**
* @Description 基本的删除单个节点的操作
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void deleteTest() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
curatorFramework.delete().forPath("/app1");
}
/**
* @Description 删除该节点以及该节点下面的子节点
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void deleteTest2() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
curatorFramework.delete().deletingChildrenIfNeeded().forPath("/app4");
}
/**
* @Description 强制删除,保证一定会删除,本质就是多发送了几次删除请求,目的是防止网络问题,出现删除不成功的现象
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void deleteTest3() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
curatorFramework.delete().guaranteed().forPath("/app1");
}
/**
* @Description 带有回调的删除
* 回调的意思是执行完当前操作,会返回来一定的内容
* @Date 2021/6/6 9:48
* @Param []
* @Return void
*/
@Test
public void deleteTest4() throws Exception {
//这个 app1 是 ming 目录下面的,因为在最上面创建的时候指定了目录为 ming
curatorFramework.delete().guaranteed().inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
System.out.println("执行了删除操作");
System.out.println(event);
}
}).forPath("/app1");
}
@After
public void close() {
if (curatorFramework != null) {
curatorFramework.close();
}
}
}
2、节点的监听器
zk允许用户在指定节点上注册一些watcher(监听器),并且在一些特定事件触发的时候,zk服务端会将事件通知到感兴趣的客户端上去,该机制是zk实现分布式协调服务的重要特性。
zk中引入了watcher机制来实现发布订阅功能,能够让多个订阅者同时监听某一个对象,当一个对象自身状态变化时,会通知所有的订阅者。
如图所示,三个App都在zk下面,其中如果App1改了自己的内容,zk会通知另外两个app,让其也进行更改。
监听器的使用
public class CuratorWatcherTest {
private CuratorFramework curatorFramework;
/**
* 建立连接
*/
@Before
public void testConnect() {
//连接的策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
/*
* Create a new client
*
* @param connectString 连接的地址
* @param sessionTimeoutMs session超时时间
* @param connectionTimeoutMs 连接超时时间
* @param retryPolicy 重试连接的策略
* @return client
*/
//第一种方式
//curatorFramework = CuratorFrameworkFactory.newClient("192.168.162.128:2181", 60 * 1000, 15 * 1000, retryPolicy);
开启连接
//curatorFramework.start();
//------------------------------------------------------------------------------------------------------------------------------
//第二种方式;可以添加默认的命名空间,以后节点都会放到该命名空间的节点下面
curatorFramework = CuratorFrameworkFactory.builder().connectString("192.168.162.128:2181").sessionTimeoutMs(60 * 1000).connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy).namespace("ming").build();
//开启连接
curatorFramework.start();
}
//======================================监听器=========================================================
/**
* 给指定节点注册监听器
*
* @throws Exception
*/
@Test
public void testNodeCache() throws Exception {
//1、创建NodeCache对象
NodeCache nodeCache = new NodeCache(curatorFramework, "/app1");
//2、注册监听
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
System.out.println("节点发生了变化~");
//获取节点的操作内容
byte[] data = nodeCache.getCurrentData().getData();
System.out.println(new String(data));
}
});
//3、开启监听,如果设置为true,则开始监听的时候,加载缓冲数据
nodeCache.start(true);
//保证该方法可以一直执行
while (true) {
}
}
/**
* 监听某个节点的子节点
*
* @throws Exception
*/
@Test
public void testNodeCache2() throws Exception {
//1、创建NodeCache对象
PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorFramework, "/app2", true);
//2、绑定监听器
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
System.out.println("子节点发生了变化!!");
System.out.println(pathChildrenCacheEvent);
//1、获取类型
PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
//2、判断类型是否为update
if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){
byte[] data = pathChildrenCacheEvent.getData().getData();
System.out.println(new String(data));
}
}
});
//3、开启
pathChildrenCache.start();
//保证该方法可以一直执行
while (true) {
}
}
/**
* 使用 TreeCache 监听某个节点自己和所有子节点们,可以监控整个树上的所有节点
*
* @throws Exception
*/
@Test
public void testNodeCache3() throws Exception {
//1、创建NodeCache对象
TreeCache treeCache = new TreeCache(curatorFramework,"/app2");
//2、注册监听
treeCache.getListenable().addListener(new TreeCacheListener() {
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
System.out.println("节点发生了变化");
System.out.println(treeCacheEvent);
}
});
//3、开启
treeCache.start();
//保证该方法可以一直执行
while (true) {
}
}
@After
public void close() {
if (curatorFramework != null) {
curatorFramework.close();
}
}
}
3、分布式锁
单机应用开发的时候,涉及到并发同步的时候,往往采用synchronized或者lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个jvm下面的,这没有问题,但是当我们的应用是分布式集群工作情况下,不属于jvm下的工作环境,跨jvm之间已经无法通过多线程的锁解决同步问题,就需要其他的锁机制,来解决跨机器进程之间的数据同步问题,这个机制就是分布式锁。
zk分布式锁原理
核心思想:当客户端想要获取锁,则创建节点,使用完锁之后,删除该节点
1、客户端获取锁时,在lock节点下创建临时顺序节点
为什么要临时:
因为如果一个客户端想要使用该锁(该锁只有一把),是用完之后应该删除掉,给其他客户端来进行使用,这个时候如果该客户端突然宕机了,如果不是临时节点,则节点不会消失,锁也不会释放,其他客户端就会处于等待的状态。而临时锁的意思是如果当前会话消失则锁会被释放。
为什么要顺序:
2、获取lock下面的所有子节点,如果发现自己创建的子节点是所有子节点中最小的一个,则获取该锁,使用完成之后,释放该锁,并删除该节点。
3、如果发现自己创建的节点并非是最小的,则不会获取锁,然后找到离自己最近并且小的节点,对其进行监听,监听删除的事件
4、如果发现比自己小的那个节点被删除了,客户端会收到通知,然后判断自己创建的节点是否是所有子节点中最小的一个,如果是,则获取该锁。
模拟12306卖票的场所
public class Ticket12306 implements Runnable {
private int ticket = 100;
//分布式锁
private InterProcessMutex lock;
//使用构造方法来创建锁
//该类被调用的时候,会在zk里面建立一个lock的目录
public Ticket12306( ) {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString("192.168.162.128:2181").sessionTimeoutMs(60 * 1000).connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy).namespace("ming").build();
//开启连接
curatorFramework.start();
lock = new InterProcessMutex(curatorFramework,"/lock");
}
@Override
public void run() {
while (true) {
//获取锁
try {
//如果获取不到,3s后在执行
lock.acquire(3, TimeUnit.SECONDS);
if (ticket > 0) {
System.out.println(Thread.currentThread() + ":" + ticket);
ticket--;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
//无论如何都要将锁进行释放,所以要用finally
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
模拟买票的场所
public class LockTest {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
Thread thread1 = new Thread(ticket12306, "携程");
Thread thread2 = new Thread(ticket12306, "飞猪");
thread1.start();
thread2.start();
}
}
这个时候zk里面会有两个临时顺序节点,节点小的会先获取锁