文章目录
一、初始Zookeeper
概念:
- 📘
zookeeper
是Apache Hadoop
项目下的一个子项目,是一个分布式协调服务用于管理分布式应用程序,翻译过来就是动物管理员,其目录结构是同Linux
的目录结构一样是一个树形结构。 - 📘
zookeeper
的主要用来干嘛:- 📖 配置管理
- 📖 分布式锁
- 📖 集群管理
- 📖 注册中心
下载: 🔗 https://zookeeper.apache.org/releases.html
安装:
-
📕 安装单机版:
- 1️⃣ 安装
zookeeper
的前提需要安装JDK
, - 🔗 https://www.runoob.com/w3cnote/win7-linux-java-setup.html
- 2️⃣ 将下载好的安装包上传到
linux
服务器,如果你使用的远程连接工具是FinalShell
,那么你可以点击底栏的倒数第二个图标就可以上传文件。 - 3️⃣ 解压到指定的目录即可:
tar -zxvf 安装包 -C /path
- 4️⃣ 解压完成后需要进入
conf
目录,修改配置文件mv zoo_sample.cfg zoo.cfg
注意,一定要修改为zoo.cfg
否则会找不到该文件。 - 5️⃣ 然后进入
zoo.cfg
文件,修改文件临时存储的位置为自己指定的位置,这里我指定在bin
目录同级
- 1️⃣ 安装
-
-
6️⃣ 最后分别启动服务端和客户端:
./zkServer.sh start
./zkCli.sh
zoo.cfg
配置文件参数说明:
- ☘️
tickTime=2000
通信心跳时间,是指zookeeper
的服务端和客户端之间的通信心跳时间,如果超过了这个时间就会认为其中的一端挂掉了。 - ☘️
initLimit=10
领导者和追随者之间初次通信的时间限制次数,这里便是initLimit * tickTime
也就是说这里最多会等待20秒。 - ☘️
syncLimit=5
领导者和追随者之间数据的同步时间限制次数,如果Leader
认为Follwer
超过了syncLimit * tickTime
那么,Leader
就会认为Follwer
已经挂掉,就会删除改节点。 - ☘️
dataDir
安装时修改的配置文件路劲,默认是tmp
路径下,而默认路径在一段时间后就会自动清理掉,所以我们会更改文件存储的路径。 - ☘️
clientPort
客户端的端口
zookeeper
集群搭建:
💅 PS: 搭建zk集群至少3台服务器,集群数量达到一半就可以正常运行,搭建集群时最好是奇数台服务器这样能更好的体现zk的性能。
-
1️⃣ 在安装单机版的基础上,创建一个名为
myid
的文件夹放在zoo.cfg中的dataDir路径下
,而且该文件必须叫myid
,在该文件中添加一个身份标识,如IP: 192.168.23.121
,就需要在该文件中输入1
,以此类推。 -
2️⃣ 在
zoo.cfg
配置文件中添加如下内容:#######################cluster########################## server.1=192.168.23.121:2182:3182 server.2=192.168.23.122:2182:3182 server.3=192.168.23.123:2182:3182
- 参数说明:
server.A=B:C:D
- A: 表示这是第几号服务器,需要和
myid
文件中的值一一对应 - B: 每天服务器的地址
- C:
Leader
和follwers
的信息交互端口 - D:
Leader
挂了重新选举时用来通信的端口
- A: 表示这是第几号服务器,需要和
- 参数说明:
-
3️⃣ 启动集群:
./zkServer.sh start
,查看状态./zkServer.sh status
如果你现在有三台服务器,那么当你启动第一台服务器时会出现ERROR
的提示,那是因为集群的数量还没有达到一半,所以你只需要再启动一台就不会报错了。
二、Zookeeper
命令操作:
Ⓜ️zk
的数据模型:
- 📖 拥有树形目录的
zk
是一个很有层次化的结构
- 📖
zk
中的每一个节点被称为:ZNode
,每个节点都会保存自己的数据和节点信息,同时也是允许保存少量内容(1MB)
在该节点下。 - 📖 分类:
- 🍋
persistent
持久化节点 - 🍋
ephemeral
临时节点 : -e - 🍋
persistent_sequential
持久化顺序节点: -s - 🍋
ephemeral_sequential
临时顺序节点: -es
- 🍋
Ⓜ️ 服务端常用命令:
- 📖 启动
zk
服务,./zkServer.sh start
- 📖 查看
zk
状态,./zkServer.sh status
- 📖停止
zk
服务,./zkServer.sh stop
- 📖重启
zk
服务,./zkServer.sh restart
Ⓜ️ 客户端常用命令:
- 📖 连接
zk
服务端,./zkCli.sh -server ip:port
- 📖 断开连接,
quit
- 📖 设置节点值,
set /节点path value
- 📖 删除单个节点,
delete /节点path
- 📖删除带有子节点的节点,
deleteall /节点path
- 📖显示指定目录下的节点,
ls 路径
- 📖创建节点,
create /节点path value
- 📖创建临时节点,
create -e /节点path value
- 📖创建顺序节点,
create -s /节点path value
- 📖创建临时顺序节点,
create -es /节点path value
- 📖 获得节点值,
get /节点path
- 📖 获得帮助,
help
- 📖 查看节点详情,
ls -s /节点path
- 🍁
cZxid
节点被创建的事物ID - 🍁
ctime
创建时间 - 🍁
mZxid
最后一次被更新的事物ID - 🍁
mtime
修改时间 - 🍁
pZxid
子节点列表最后一次被更新的事物ID - 🍁
cversion
子节点的版本号 - 🍁
dataversion
数据版本号 - 🍁
aclversion
权限版本号 - 🍁
ephemeralOwner
用于临时节点,代表临时节点是事物ID,如果为持久节点那么ID为0 - 🍁
dataLength
节点存储数据的长度 - 🍁
numChildren
当前节点的子节点个数
- 🍁
三、JavaAPI
操作:
zk
客户端库的介绍:
-
📖 原生
JavaAPI
,最难用 -
📖 ZKClient,比原生的好点
-
📖Curator,相比前两种最好的,是Netfix公司研发的后来捐给了
Apache
基金会,目前是Apache
基金会的顶级项目
Curator
常用API的操作:
-
📖 建立连接:
-
1️⃣ 导入依赖
<!--curator--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.0</version> </dependency> <!--日志--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency>
-
2️⃣ 日志文件:
log4j.rootLogger=off,stdout log4j.appender.stdout = org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target = System.out log4j.appender.stdout.layout = org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern = [%d{yyyy-MM-dd HH/:mm/:ss}]%-5p %c(line/:%L) %x-%m%n
-
3️⃣ : 建立连接:
import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.junit.Test; public class CuratorTest { @Before public void connectionTest(){ //建立连接的两种方式 // 每间隔3秒重试一次,一共重试10次 RetryPolicy policy = new ExponentialBackoffRetry(3000,10); //1.第一种 /** * @param connectString 连接信息list of servers to connect to * @param sessionTimeoutMs 会话超时时间session timeout * @param connectionTimeoutMs 连接超时时间connection timeout * @param retryPolicy 建立连接失败的重试策略retry policy to use */ CuratorFramework client = CuratorFrameworkFactory.newClient("IP:port", 60 * 1000, 15 * 1000, policy); client.start(); //2.第二种,通过链式编程的方式 CuratorFramework client2 = CuratorFrameworkFactory .builder() .connectString("IP:port") .sessionTimeoutMs(60 * 1000) .connectionTimeoutMs(15 * 1000) .retryPolicy(policy) // 当然你还可以指定名称空间,意思就是当你创建一个节点的时候默认在节点前面添加前缀 .namespace("csdn") .build(); client2.start(); } }
如果你和我一样使用的是云服务器,那么记得开放2181端口
-
-
📖 添加节点:
-
@Test public void createTest1() throws Exception { //创建持久节点 client.create().forPath("/app1","myapp".getBytes()); } @Test public void createTest2() throws Exception { //创建临时顺序节点 client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/app2","myapp".getBytes()); } @Test public void createTest3() throws Exception { //创建持久顺序节点 client.create().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/app3","myapp".getBytes()); } @Test public void createTest4() throws Exception { //创建多级持久顺序节点 client .create() .creatingParentsIfNeeded() // 如果需要创建多级节点需要添加此参数 .withMode(CreateMode.PERSISTENT_SEQUENTIAL) .forPath("/app4/children","myapp".getBytes()); }
-
-
📖 删除节点:
-
@Test public void deleteTest1() throws Exception { // 删除单个 client.delete().forPath("/app1"); } @Test public void deleteTest2() throws Exception { // 删除多个 client.delete().deletingChildrenIfNeeded().forPath("/app4"); } @Test public void deleteTest3() throws Exception { // 必须删除 client.delete().guaranteed().forPath("/app30000000003"); }
-
-
📖 修改节点:
-
@Test public void setTest() throws Exception { // 修改数据 int version = 0; Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath("/app1"); version = stat.getVersion(); System.out.println(version); client.setData().withVersion(version).forPath("/app1","myapp3".getBytes()); }
-
-
📖 查询节点:
-
@Test public void getTest1() throws Exception { // 获取节点数据 byte[] bytes = client.getData().forPath("/app1"); System.out.println(new String(bytes)); } @Test public void getTest2() throws Exception { // 获取子节点数据 List<String> childrens = client.getChildren().forPath("/app4"); for (String children : childrens) { System.out.println(children); } } @Test public void getTest3() throws Exception { // 获取节点状态信息 Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath("/app1"); System.out.println(stat.getDataLength()); }
PS: 如果你想要获取顺序节点的内容那么你需要进行序列化
-
-
📖
Watch
事件监听: -
zk
可以允许用户在指定节点上注册一些监听器(Watcher
), 并且在触发特定事件时通知其它节点,这一机制就是zk
中实现分布式协调服务的重要特性。 -
原生的API 操作监听器十分不方便,故此有了
Curator
,Curator
中使用Cache
来实现对zk
服务端事件的监听。 -
种类:
- 🍋
NodeCache
: 只监听某个特定的节点 - 🍋
PathChildrenCache
: 监听一个节点下的子节点 - 🍋
TreeCache
: 监听整个树上的所以节点
- 🍋
-
NodeCache
:@Test public void nodeCacheTest() throws Exception { // 创建监听器 NodeCache cache = new NodeCache(client,"/app",false); cache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { System.out.println("监听到app数据变化"); byte[] data = cache.getCurrentData().getData(); System.out.println(new String(data)); } }); cache.start(true); // 是否初始化时缓存数据 while (true){} // 为了让程序不停止 }
-
PathChildrenCache
@Test public void pathChildrenCacheTest() throws Exception { //当然你还可以传入自定义的线程池,以及是否压缩数据 PathChildrenCache pathChildren = new PathChildrenCache(client,"/app",true); pathChildren.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { System.out.println("监听到子节点变化了"); //对监听的类型进行判断 if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){ byte[] data = event.getData().getData(); System.out.println("监听到的数据:" + new String(data)); } } }); pathChildren.start(true); while (true){} }
-
ThreeCache
@Test public void threeCacheTest() throws Exception { TreeCache treeCache = new TreeCache(client, "/app"); treeCache.getListenable().addListener(new TreeCacheListener() { @Override public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception { System.out.println("不仅可以监听自己而且还可以监听子节点"); byte[] data = event.getData().getData(); System.out.println(new String(data)); } }); treeCache.start(); while (true){} }
-
📖 分布式锁的实现:
在进行单机应用开发时在对数据并发同步时往往都是采用
syncchronized
或者Lock
进行加锁的方式取解决数据同步问题,但是在跨机器时的数据同步问题采用这种方式就不可以了,这时就需要使用分布式锁来实现数据同步。
-
锁的分类:
- 🌻
InterProcessSemaphoreMutex
: 分布式非可重入锁 - 🌻
InterProcessMutex
: 分布式可重入锁 - 🌻
InterProcessReadWriteLock
: 分布式读写锁 - 🌻
InterProcessMultiLock
: 多锁单用 - 🌻
InterProcessSemaphoreV2
: 共享信号量
- 🌻
-
实现:
- 模拟买票:
import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.apache.curator.retry.ExponentialBackoffRetry; import java.util.concurrent.TimeUnit; public class Ticket12306 implements Runnable{ private int ticket = 10; /** * 使用分布式锁 */ private InterProcessMutex lock; public Ticket12306(){ RetryPolicy policy = new ExponentialBackoffRetry(3000,10); CuratorFramework client = CuratorFrameworkFactory .newClient("43.142.107.50:2181", 60 * 1000, 15 * 1000, policy); client.start(); lock = new InterProcessMutex(client,"/app"); } @Override public void run() { while (true){ //获得锁 try { lock.acquire(3, TimeUnit.SECONDS); if (ticket > 0){ System.out.println(Thread.currentThread() + "卖了第" + ticket + "张票"); ticket --; } } catch (Exception e) { e.printStackTrace(); }finally { try { //释放锁 lock.release(); } catch (Exception e) { e.printStackTrace(); } } } } }
public class SellTicket { public static void main(String[] args) { Ticket12306 ticket = new Ticket12306(); Thread t1 = new Thread(ticket, "携程"); Thread t2 = new Thread(ticket, "飞猪"); t1.start(); t2.start(); } }
四、核心理论:
选举机制:
首次和二次选举,票数达到集群的一半以上即为Leader,此处假设五台机器
-
✋ 首次启动时选举:
- 每台机器都有选举自己的一票,当第一次启动时,第一台机器选举自己此时它只有一票未达到总的一半以上便不是
Leader
- 第二台机器启动,进行选举,此时会根据
myid
文件中的number
进行比较大小,值小的将自己的票给值大的,假设第二台机器的值大于第一台,此时第二台拥有两票,未达到一半以上不是Leader
- 第三台机器启动,进行选举,假设第三台的
myid
中的number
大于第二台中myid
的number
那么第二台机器就会将手中的两票投给第三台机器,此时第三台机器则拥有1+2
的票数,达到一半以上则为Leader
,如果一旦确认了Leader
则其它机器便默认为follwer
。
- 每台机器都有选举自己的一票,当第一次启动时,第一台机器选举自己此时它只有一票未达到总的一半以上便不是
-
✋ 二次选举:
- 此时假设第五台机器无法和
Leader
保持通信了,开始第二次选举。 - 选举时的两种情况:
Leader
任然存在- 此时第五台机器会进行重试与
Leader
进行通信,如果能通信成功则与Leader
进行同步,如果不能成功则默认Leader
也挂了。
- 此时第五台机器会进行重试与
Leader
已经挂了- 这时第三台机器和第五台机器已经挂了,这
1,2,4
进行选举,选举的依据是:【Epoch
(领导者任期编号) 、ZXID
(事物ID) 、SID
(服务器ID)】,根据Epoch > ZXID > SID
的规则进行选举。
- 这时第三台机器和第五台机器已经挂了,这
- 此时假设第五台机器无法和
zk 中的分布式锁原理:
- 📖 核心思想:客户端想要获得锁,那么就需要创建节点,使用完锁,删除节点。
- 1️⃣ 当客户端想要获得锁时会在对应节点创建一个临时顺序节点,使用完了以后删除该节点。
- 2️⃣ 然后会获得该节点下的所有子节点,根据节点顺序值比较大小,最小的则会获得到锁,如果不是最小的则大的节点会在自己的前一个节点注册一个监听器,用来监听删除事件。
- 3️⃣ 如果发现前一个节点触发了删除事件,那么会再一次进行比较谁的值最小,由最小值的节点获得锁。