2021-01-28 大数据课程笔记 day8

时间煮雨
@R星校长

ZooKeeper 基础
为什么使用 ZooKeeper?

集群存在大量服务器时,出现故障时在所难免的,那么如何快速知道哪些服务器出现故障?
在这里插入图片描述
在这里插入图片描述

  1. Nginx 作为负载均衡管理大量服务器时,管理起来比较麻烦,可以通过 zookeeper 注册服务与发现服务协作管理。
  2. 以前大部分应用需要开发私有的协调程序,缺乏一个通用的机制协调程序的反复编写浪费,且难以形成通用、伸缩性好的协调器
  3. 使用分布式部署后,多线程安全的问题,以前学的同步代码块、重构锁、读写锁等通通失效,怎么办?
ZooKeeper概述
ZooKeeper简介

ZooKeeper:动物园管理员
ZooKeeper 是分布式应用程序的协调服务框架,是 Hadoop 的重要组件。ZooKeeper 是 Google 的 Chubby 一个开源的实现,是 Hadoop 的分布式协调服务,包含一个简单的原语集,分布式应用程序可以基于它实现。
扩展:zk 是根据 Google 的一篇论文
《The Chubby lock service for loosely coupled distributed systems》

具体应用场景
  1. Hadoop , 使用 ZooKeeper 的事件处理确保整个集群只有一个 NameNode ,存储配置信息等。
    在这里插入图片描述
  2. HBase , 使用 ZooKeeper 的事件处理确保整个集群只有一个 HMaster ,察觉 HRegionServer 联机和宕机,存储访问控制列表等。
分布式编程容易出现的问题

分布式的思想就是人多干活快,即用多台机器同时处理一个任务。分布式的编程和单机的编程 思想是不同的,随之也带来新的问题和挑战。

  1. 活锁。
    活锁定义:在程序里,由于某些条件的发生碰撞,导致重新执行,再碰撞=》再执 行,如此循环往复,就形成了活锁。活锁的危害:多个线程争用一个资源,但是没有任何一个 线程能拿到这个资源。(死锁是有一个线程拿到资源,但相互等待互不释放造成死锁),活锁 是死锁的变种。补充:活锁更深层次的危害,很耗尽 Cpu 资源(在做无意义的调度)
  2. 需要考虑集群的管理问题,需要有一套机制来检测到集群里节点的状态变化。
  3. 如果用一台机器做集群管理,存在单点故障问题,所以针对集群管理,也需要形成一个集群
  4. 管理集群里 Leader 的选举问题(要根据一定的算法和规则来选举),包括要考虑 Leader 挂掉 之后,如何从剩余的 follower 里选出 Leader
  5. 分布式锁的实现,用之前学的重入锁,同步代码块是做不了的
Paxos 的小岛的故事

那么 ZooKeeper 最基础的东西是什么呢?不得不提 Paxos,它是一个基于消息传递的一致性算法,Leslie Lamport(莱斯利·兰伯特)在 1990 年提出,近几年被广泛应用于分布式计算中,Google 的Chubby,Apache 的 ZooKeeper 都是基于它的理论来实现的,Paxos 还被认为是到目前为止唯一的分布式一致性算法,其它的算法都是 Paxos 的改进或简化。有个问题要提一下,Paxos 有一个前提:没有拜占庭将军问题。就是说 Paxos 只有在一个可信的计算环境中才能成立,这个环境是不会被入侵所破坏的。

Paxos 描述了这样一个场景,有一个叫做 Paxos 的小岛 ( Island ) 上面住了一批居民,岛上面所有的事情由一些特殊的人决定,他们叫做议员 ( Senator )。议员的总数 ( Senator Count ) 是确定的,不能更改。岛上每次环境事务的变更都需要通过一个提议 ( Proposal ) ,每个提议都有一个编号 ( PID ) ,这个编号是一直增长的,不能倒退。每个提议都需要超过半数 (( Senator Count ) / 2 +1 ) 的议员同意才能生效。每个议员只会同意大于当前编号的提议,包括已生效的和未生效的。如果议员收到小于等于当前编号的提议,他会拒绝,并告知对方:你的提议已经有人提过了。这里的当前编号是每个议员在自己记事本上面记录的编号,他不断更新这个编号。整个议会不能保证所有议员记事本上的编号总是相同的。现在议会有一个目标:保证所有的议员对于提议都能达成一致的看法。

好,现在议会开始运作,所有议员一开始记事本上面记录的编号都是 0。有一个议员发了一个提议:将电费设定为 1 元/度。他首先看了一下记事本,嗯,当前提议编号是 0,那么我的这个提议的编号就是 1,于是他给所有议员发消息:1 号提议,设定电费1元/度。其他议员收到消息以后查了一下记事本,哦,当前提议编号是 0,这个提议可接受,于是他记录下这个提议并回复:我接受你的1号提议,同时他在记事本上记录:当前提议编号为 1 。发起提议的议员收到了超过半数的回复,立即给所有人发通知:1 号提议生效!收到的议员会修改他的记事本,将1好提议由记录改成正式的法令,当有人问他电费为多少时,他会查看法令并告诉对方:1 元 / 度。

现在看冲突的解决:假设总共有三个议员 S1 - S3 ,S1 和 S2 同时发起了一个提议:1号提议,设定电费。S1 想设为 1 元 / 度, S2 想设为 2 元 / 度 。结果 S3 先收到了 S1 的提议,于是他做了和前面同样的操作。紧接着他又收到了 S2 的提议,结果他一查记事本,咦,这个提议的编号小于等于我的当前编号 1,于是他拒绝了这个提议:对不起,这个提议先前提过了。于是 S2 的提议被拒绝,S1 正式发布了提议: 1 号提议生效。S2 向 S1 或者 S3 打听并更新了 1 号法令的内容,然后他可以选择继续发起 2 号提议。

好,我觉得 Paxos 的精华就这么多内容。现在让我们来对号入座,看看在 ZK Server 里面 Paxos 是如何得以贯彻实施的。

小岛(Island)——ZK Server Cluster
议员(Senator)——ZK Server
提议(Proposal)——ZNode Change(Create/Delete/SetData…)
提议编号(PID)——Zxid(ZooKeeper Transaction Id)
正式法令——所有ZNode及其数据

貌似关键的概念都能一一对应上,但是等一下,Paxos 岛上的议员应该是人人平等的吧,而 ZK Server 好像有一个 Leader 的概念。没错,其实 Leader 的概念也应该属于 Paxos 范畴的。如果议员人人平等,在某种情况下会由于提议的冲突而产生一个“活锁”(所谓活锁我的理解是大家都没有死,都在动,但是一直解决不了冲突问题)。Paxos 的作者 Lamport 在他的文章 “ The Part-Time Parliament ” 中阐述了这个问题并给出了解决方案——在所有议员中设立一个总统,只有总统有权发出提议,如果议员有自己的提议,必须发给总统并由总统来提出。
好,我们又多了一个角色:总统。
总统 —— ZK Server Leader
在这里插入图片描述

ZooKeeper集群

攘其外:消息队列

安其内:选举

现在我们假设总统已经选好了,下面看看 ZK 是怎么实施的。

情况一:
总统突然挂了,议员接二连三的发现联系不上总统,于是各自发表声明,推选新的总统,总统大选期间政府停业,拒绝屁民的请求。呵呵,到此为止吧,当然还有很多其他的情况,但这些情况总是能在 Paxos 的算法中找到原型并加以解决。这也正是我们认为 Paxos 是
ZooKeeper 的灵魂的原因。当然 ZK 还有很多属于自己特性的东西:Session , Watcher ,Version 等等。

情况二:
屁民甲 ( Client ) 到某个议员 ( ZK Server ) 那里询问 ( Get ) 某条法令的情况 ( ZNode 的数据) ,议员毫不犹豫的拿出他的记事本 ( local storage ) ,查阅法令并告诉他结果,同时声明:我的数据不一定是最新的。你想要最新的数据?没问题,等着,等我找总统 Sync 一下再告诉你。

情况三:
屁民乙 ( Client ) 到某个议员 ( ZK Server ) 那里要求政府归还欠他的一万元钱,议员让他在办公室等着,自己将问题反映给了总统,总统询问所有议员的意见,多数议员表示欠屁民的钱一定要还,于是总统发表声明,从国库中拿出一万元还债,国库总资产由 100 万变成 99 万。屁民乙拿到钱回去了 ( Client 函数返回 ) 。

ZooKeeper 集群分布式安装
四台服务器之间免密登录

四台服务器之间互相均可以免密登录
a、 首先在四台服务器上都要执行:

ssh-keygen  -t  dsa  -P  ''  -f  ~/.ssh/id_dsa

b、在 node1 上将 node1 的公钥拷贝到 authorized_keys 中:

cat  ~/.ssh/id_dsa.pub  >>  ~/.ssh/authorized_keys

将该文件拷贝给 node2:

scp  ~/.ssh/authorized_keys   node2:/root/.ssh/

c、在 node2 中将 node2 的公钥追加到 authorized_keys 中:

cat  ~/.ssh/id_dsa.pub  >>  ~/.ssh/authorized_keys

将该文件拷贝给 node3:

scp  ~/.ssh/authorized_keys   node3:/root/.ssh/

d、在 node3 中将 node3 的公钥追加到 authorized_keys 中:

cat  ~/.ssh/id_dsa.pub  >>  ~/.ssh/authorized_keys

将该文件拷贝给 node4:

scp  ~/.ssh/authorized_keys   node4:/root/.ssh/

e、在 node4 中将 node4 的公钥追加到 authorized_keys 中:

cat  ~/.ssh/id_dsa.pub  >>  ~/.ssh/authorized_keys

将该文件拷贝给 node1 、node2 、node3:

scp  ~/.ssh/authorized_keys   node1:/root/.ssh/
scp  ~/.ssh/authorized_keys   node2:/root/.ssh/
scp  ~/.ssh/authorized_keys   node3:/root/.ssh/
JDK 安装环境变量配置

node1 - node4

mkdir /opt/apps

将 jdk-8u221-linux-x64.rpm 上传到 node1/opt/apps

将 /opt/apps 下的 jdk.rpm scp 到 node2 、node3 、node4 的对应目录中

scp jdk-8u221-linux-x64.rpm node2:/opt/apps
scp jdk-8u221-linux-x64.rpm node3:/opt/apps
scp jdk-8u221-linux-x64.rpm node4:/opt/apps

在 node1 、node2 、node3 、node4 上安装 jdk 并配置 profile 文件

rpm -ivh jdk-8u221-linux-x64.rpm

node1 上修改环境变量

vim /etc/profile
export JAVA_HOME=/usr/java/default
export PATH=$PATH:$JAVA_HOME/bin
source /etc/profile

将 node1 的 /etc/profile 拷贝到 node2 、node3 、node4 上并执行 . /etc/profile

scp /etc/profile node[234]:`pwd`
ZooKeeper 集群搭建

 a) 将 ZooKeeper.tar.gz 上传到 node2
 b) 解压到 /opt

tar -zxvf ZooKeeper-3.4.6.tar.gz -C /opt

 c) 配置环境变量:

export ZOOKEEPER_HOME=/opt/zookeeper-3.4.6
export PATH=$PATH:$JAVA_HOME/bin:$ZOOKEEPER_HOME/bin

  然后 ./etc/profile 让配置生效
  最后将该文件 scp 到 node3 和 node4 上,并分别 ./etc/profile 让配置生效.

scp /etc/profile bk3:/etc/
scp /etc/profile bk4:/etc/

 d) 到 $ZooKeeper_HOME/conf 下
  复制 zoo_sample.cfg 为 zoo.cfg

cp zoo_sample.cfg  zoo.cfg

 e) 编辑 zoo.cfg
补充:参数说明

tickTime=2000 #发送心跳的间隔时间,单位:毫秒
dataDir=/opt/zookeeper-3.4.6/data #ZooKeeper保存数据的目录
dataLogDir=/var/bjsxt/zookeeper/datalog #日志目录
clientPort=2181
initLimit=5
syncLimit=2
server.1=server2:2881:3881
server.2=server3:2881:3881  
server.3=node4:2881:3881  #observer(表示对应节点不参与投票)

clientPort:客户端连接 ZooKeeper 服务器的端口,ZooKeeper 会监听这个端口,接受客户端的访问请求。

initLimit: 这个配置项是用来配置 ZooKeeper 接受客户端(这里所说的客户端不是用户连接ZooKeeper服务器的客户端,而是 ZooKeeper 服务器集群中follower或observer连接到 Leader的Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 5 个心跳的时间(也就是 tickTime)长度后 ZooKeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 5*2000=10秒

syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 4*2000=8 秒
server.A=B:C:D:其 中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 ZooKeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

添加如下行:

server.1=node2:2881:3881
server.2=node3:2881:3881
server.3=node4:2881:3881

修改

dataDir=/opt/ZooKeeper-3.4.6/data

 f) 创建 /opt/ZooKeeper-3.4.6/data 目录,并在该目录下放一个文件:myid
  在 myid 中写下当前 ZooKeeper 的编号

mkdir /opt/zookeeper-3.4.6/data
mkdir /var/bjsxt/zookeeper/datalog
echo 1 > /opt/zookeeper-3.4.6/data/myid

 g) 将配置好 ZooKeeper 拷贝到 node3 、node4 上

scp -r ZooKeeper-3.4.6/ node3:/opt/
scp -r ZooKeeper-3.4.6/ node4:/opt/

 h) 在 node3 和 node4 上分别修改:myid

node3

echo 2 > /opt/ZooKeeper-3.4.6/data/myid
2

node4

echo 3 > /opt/ZooKeeper-3.4.6/data/myid
3

 i) 分别启动 ZooKeeper

	zkServer.sh start 启动zk
	zkServer.sh stop  停止zk
	zkServer.sh status  查看zk状态
	zkServer.sh start|stop|status

 j) 关闭 ZooKeeper

zkServer.sh stop

 l) 连接 ZooKeeper

zkCli.sh # node2 、node3 、node4 都可以

 m) 退出 zkCli.sh 命令

quit

提供集群模式的服务

原子性
    准确的反馈成功或失败
一致性
    每个 server 都有统一的数据视图
可用性
    节点故障不影响使用    少于一半的宕机,没问题

网络分区 / 脑裂:过半通过
谁年龄大 myid:

1 2 3  (同时启动)
1 2 3   (逐一启动)

谁数据新:

3 3 4  事务 id( zxid )大的当领导
 3台机器 挂一台	2>3/2
 5台机器 挂了1台 4>5/2(可以工作) 挂3台 2!>5/2(集群无法工作)
顺序性:FIFO
主从模型
	Leader  Follower+
    一写多读
		Leader负责增删改,Follower负责读和投票
集群状态
选举模式  安其内
广播模式  壤其外
Server状态
LOOKING:当前Server不知道leader是谁,正在搜寻
LEADING:当前Server即为选举出来的leader
FOLLOWING:leader已经选举出来,当前Server与之同步
主从分工
   领导者(leader)
    	负责进行投票的发起和决议,更新系统状态,(增删改)
   学习者(learner)
        包括跟随者(follower)和观察者(observer),follower用于接受客户端
        请求并向客户端返回结果,在操作过程中参与投票
   Observer
   可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,
   只同步leader的状态,observer的目的是为了扩展系统,提高读取速度
   客户端(client)
    	请求发起方
ZooKeeper 进阶
Znode 数据结构

在这里插入图片描述

  1. ZK 有一个最开始的节点 /
  2. ZK 的节点叫做 znode 节点
  3. 每个 znode 节点都可存储数据
  4. 每个 znode 节点(临时节点除外)都可创建自己的子节点
  5. 多个 znode 节点共同形成了 znode 树
  6. Znode 树的维系实在内存中,目的是供用户快速的查询
  7. 每个 znode 节点都是一个路径(通过路径来定位这个节点)
  8. 每个路径名都是唯一的
目录结构

层次的,目录型结构,便于管理逻辑关系
znode 信息
  包含最大 1MB 的数据信息
  记录了 zxid 等元数据信息

节点类型

znode 有两种类型,临时的( ephemeral )和持久的( persistent )
znode 支持序列 SEQUENTIAL
  临时 znode
    客户端会话结束时,ZooKeeper 将该临时 znode 删除,临时 znode 没有子节点
  持久 znode
    不依赖于客户端会话,只有当客户端明确要删除该持久 znode 时才会被删除
  znode 的类型在创建时确定并且之后不能再修改

  有序 znode 节点被分配唯一单调递增的整数。
    比如:客户端创建有序 znode ,路径为 /task/task- ,则 ZooKeeper 为其分配序号 1 ,并追加到 znode 节点:
     /task/task-000000001 。有序 znode 节点唯一,同时也可根据该序号查看 znode 创建顺序。

znode 有四种形式的目录节点

	PERSISTENT:普通持久
	EPHEMERAL:普通临时
	PERSISTENT_SEQUENTIAL:顺序持久
	EPHEMERAL_SEQUENTIAL:顺序临时

要想执行以下指令,需要先启动 zk 服务器端,再启动 zk 客户端

./zkServer.sh start: 启动 zk 服务器端
./zkCli.sh:启动 zk 客户端
ZK 客户端命令行操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ZooKeeper 会话

1、客户端通过 TCP 协议与独立服务器或者一个集群中的某个服务器建立会话连接。
2、会话提供顺序保障,即同一个会话中的请求以 FIFO 的顺序执行。如果客户端有多个并发会话,FIFO 顺序在多个会话之间未必能够保持。
3、如果连接的 Server 出现问题,在没有超过 Timeout 时间时,可以连接其他节点。 ZooKeeper 客户端透明地转移一个会话到不同的服务器。
4、同一 session 期内的特性不变
5、当一个会话因某种原因终止,在这个会话期间创建的临时节点将会消失。

Session 是由谁来创建的?
Leader:产生一个唯一的 session ,放到消息队列,让所有 server 知道
过半机制:保证 session 创建成功或者失败

事件监听原理刨析

客户端轮询指定节点下的数据
通过网络轮询,代价很大
在这里插入图片描述

基于通知(notification)的机制:

客户端向 ZooKeeper 注册需要接收通知的 znode ,
通过对 znode 设置监视点( watch )来接收通知。监视点是一个单次触发的操作,意即监视点会触发一个通知。
为了接收多个通知,客户端必须在每次通知后设置一个新的监视点。

在这里插入图片描述

事件监听 Watcher

Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应。

可以设置观察点的操作:exists , getChildren , getData
可以触发观察的操作:create , delete , setData
回调 client 方法
业务核心代码在哪里?
     client

广播模式刨析

ZooKeeper 的核心是原子广播,这个机制保证了各个 server 之间的信息同步。实现这个机制的协议叫做 ZAB 协议。

ZAB 协议有两种模式:

  1. 恢复模式:当服务启动或者在领导者崩溃后,ZAB 就进入了恢复模式。当领导者被选举出来,且大多数 server 的完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 follower 以及 observer 具有相同的系统状态
  2. 广播模式
    广播模式需要保证 proposal 被按顺序处理,因此 zk 采用了递增的事务 id 号 ( zxid ) 来保证。所有的提议 ( proposal ) 都在被提出的时候加上了 zxid ( 比如: 0x1000000300000002 ) 。
    epoch 也称为纪元数字。实现中 zxid 是一个 64 位的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch ,低32位是个递增计数。

广播模式:
在这里插入图片描述

Zookeeper集群的特点

在这里插入图片描述

•	角色模型
	–	集群状态(可用/不可用)
	–	主从分工
•	攘其外:
	–	统一视图
		•	会话session
		•	数据模型Znode
			–	目录结构
			–	节点类型
	–	事件监听Watcher
•	原理:
	–	原子消息广播协议ZAB
	•	paxos
		–	journalnode
		–	Sentinel (redis 哨兵)
		–	ZooKeeper  -- ZAB
	•	zxid ,myid:
	•	ZXID:epoch+ID
–	广播模式原理
–	恢复模式原理:无主模型:zab: zxid ,myid
无主,无服务。选举过程耗时在200ms之内,一般情况下ZooKeeper恢复服务时间间隔不超过200ms
ZK 常见的应用场景
  1. 分布式环境下的统一命名服务

在这里插入图片描述

  1. 分布式环境下的配置管理
    在这里插入图片描述
  2. 数据发布/订阅
  3. 分布式环境下的分布式锁
  4. 集群管理问题
ZK API实战
IDEA环境搭建
  1. 新建 project
  2. 将 log4j.properties 文件拷贝到项目的 src 目录
  3. 项目的根目录新建 lib 文件夹,将 jar 包拷贝到该目录下。
  4. File->Project Structure ->Libaries
    在这里插入图片描述
    在这里插入图片描述
  5. 新建包
    在这里插入图片描述
创建ZooKeeper客户端

在该包下新建类 ZooKeeperTest

package com.bjsxt.api;

import java.io.IOException;
import java.util.List;

import org.apache.ZooKeeper.CreateMode;
import org.apache.ZooKeeper.KeeperException;
import org.apache.ZooKeeper.WatchedEvent;
import org.apache.ZooKeeper.Watcher;
import org.apache.ZooKeeper.ZooDefs.Ids;
import org.apache.ZooKeeper.ZooKeeper;
import org.apache.ZooKeeper.data.Stat;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ZooKeeperTest {
	
	private static final int SESSION_TIMEOUT = 30000;
	
	public static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperTest.class);
	
	private Watcher watcher =  new Watcher() {

		public void process(WatchedEvent event) {
			LOGGER.info("process : " + event.getType());
		}
	};
	
	private ZooKeeper ZooKeeper;
	
	/**创建ZooKeeper连接,单元测试前先执行该方法
	 * @throws IOException
	 */
	@Before
	public void connect() throws IOException {
//		indicate : all servers 
		ZooKeeper  = new ZooKeeper("192.168.20.52:2181,192.168.20.53:2181,192.168.20.54:2181", SESSION_TIMEOUT, watcher);
	}
	
	/**测试完毕后关闭zk连接
	 */
	@After
	public void close() {
		try {
			ZooKeeper.close();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 创建 znode 
	 *  1.CreateMode  
	 *  PERSISTENT :持久化
	 *  PERSISTENT_SEQUENTIAL
	 *  EPHEMERAL :临时
	 *  EPHEMERAL_SEQUENTIAL
	 *  Access Control List: 访问控制列表
	 *  https://baike.baidu.com/item/ACL/362453?fr=aladdin
	 *  OPEN_ACL_UNSAFE: ANYONE CAN VISIT 
	 * <br>------------------------------<br>
	 */
	@Test
	public void testCreate() {
		String result = null;
		 try {
			 result = ZooKeeper.create("/zk001", "zk001data-p".getBytes(),
					 Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//			 result = ZooKeeper.create("/zk002", "zk002data-e".getBytes(),
//					 Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
//			 Thread.sleep(30000);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
//抛出AssertionError
			 Assert.fail();
		}
		 LOGGER.info("create result : {}", result);
	 }
	
	/**
	 * 删除
	 */
	@Test
	public void testDelete() {
		 try {
			ZooKeeper.delete("/zk002", -1);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
             //抛出AssertionError
			 Assert.fail();
		}
	}
	
	/**
	 * 获取数据
	 */
	@Test
	public void testGetData() {
		String result = null;
		 try {
			 byte[] bytes = ZooKeeper.getData("/zk001", null, null);
			 result = new String(bytes);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 LOGGER.info("getdata result------------------------------------------------- : {}", result);
	}

	/**查看从哪个zk服务器获取的数据,然后xshell关闭对应的zk服务器(zkServer.sh stop)
	 * 然后分析日志:
	 * @throws Exception
	 */
	@Test
	public void testGetData01() throws Exception {
		String result = null;
		try {
			byte[] bytes = ZooKeeper.getData("/zk001", null, null);
			result = new String(bytes);
		} catch (Exception e) {
			LOGGER.error(e.getMessage());
			Assert.fail();
		}
		LOGGER.info("getdata result-----------------1------------------ : {}", result);
		
		Thread.sleep(30000);
		
		byte[] bytes;
		try {
			bytes = ZooKeeper.getData("/zk001", null, null);
			result = new String(bytes);
		} catch (KeeperException | InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		LOGGER.info("getdata result-----------------2-------------------- : {}", result);
	}
	
	/**
	 *   注册
	 */
	@Test
	public void testGetDataWatch() {
		String result = null;
		 try {
			 System.out.println("get:");
			 byte[] bytes = ZooKeeper.getData("/zk001", new Watcher() {
				public void process(WatchedEvent event) {
					LOGGER.info("testGetDataWatch  watch : {}", event.getType());
					System.out.println("watcher ok");
					//testGetDataWatch();
				}
			 }, null);
			 result = new String(bytes);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 LOGGER.info("getdata result------------------------------------------ : {}", result);
		 
		 // wacth  NodeDataChanged
		 try {
			 System.out.println("set:");
			 ZooKeeper.setData("/zk001", "testSetDataWAWWW".getBytes(), -1);
			 System.out.println("set2:");
			 ZooKeeper.setData("/zk001", "testSetDataWAWWW".getBytes(), -1);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 System.out.println("over");
		try {
			Thread.sleep(30000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	@Test
	public void testExists() {
		Stat stat = null;
		 try {
			 stat = ZooKeeper.exists("/zk001", false);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 Assert.assertNotNull(stat);
		 LOGGER.info("exists result : {}", stat.getCzxid());
	}
	
	@Test
	public void testSetData() {
		Stat stat = null;
		 try {
			 stat = ZooKeeper.setData("/zk001", "testSetData".getBytes(), -1);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 Assert.assertNotNull(stat);
		 LOGGER.info("exists result : {}", stat.getVersion());	
	}
	
	@Test
	public void testExistsWatch1() {
		Stat stat = null;
		 try {
			 stat = ZooKeeper.exists("/zk001", true);
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 Assert.assertNotNull(stat);
		 
		 try {
			ZooKeeper.delete("/zk001", -1);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Test
	public void testExistsWatch2() {
		Stat stat = null;
		 try {
			 stat = ZooKeeper.exists("/zk002", new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					LOGGER.info("testExistsWatch2  watch : {}", event.getType());
				}
			 });
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
		 Assert.assertNotNull(stat);

		 try {
			ZooKeeper.setData("/zk002", "testExistsWatch2".getBytes(), -1);
		} catch (Exception e) {
			e.printStackTrace();
		}
		 
		 try {
			ZooKeeper.delete("/zk002", -1);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 */
	@Test
	public void testGetChild() {
		 try {
			 ZooKeeper.create("/zk/001", "001".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			 ZooKeeper.create("/zk/002", "002".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			 
			 List<String> list = ZooKeeper.getChildren("/zk", true);
			for (String node : list) {
				LOGGER.info("fffffff {}", node);
			}
		} catch (Exception e) {
			 LOGGER.error(e.getMessage());
			 Assert.fail();
		}
	}
}

框架实现的客户端和服务器之间的故障转移过程
注册 Watcher ,查询注册 watch ,增删改触发 watcher
如果是多次增删改,回调方法调用几次?
1、watcher 事件是一次性的
2、是增删改触发 watcher,但是 watcher 是线程异步执行
3、watcher 可以反复注册
如何平滑反复注册?在回调方法中迭代调用即可。

分布式协调案例

如何实现 “ 跨虚拟机 ” 的调用,它就是 RMI( Remote Method Invocation ,远程方法调用)。例如,服务 A 在 JVM1 中运行,服务 B 在 JVM2 中运行,服务 A 与 服务 B 可相互进行远程调用,就像调用本地方法一样,这就是 RMI 。在分布式系统中,我们使用 RMI 技术可轻松将 服务提供者( Service Provider )与 服务消费者( Service Consumer )进行分离,充分体现组件之间的弱耦合,系统架构更易于扩展。

在这里插入图片描述
我们先从通过一个最简单的 RMI 服务与调用示例,快速掌握 RMI 的使用方法,然后指出 RMI 的局限性,最后笔者对此问题提供了一种简单的解决方案,即使用 ZooKeeper 轻松解决 RMI 调用过程中所涉及的问题。

下面我们就从一个最简单的 RMI 示例开始吧!

Java 原生 RMI 实现

在这里插入图片描述

发布 RMI 服务

发布一个 RMI 服务,我们只需做三件事情:

  1. 定义一个 RMI 接口:HelloService
  2. 编写 RMI 接口的实现类: HelloServiceImpl
  3. 通过 JNDI 发布 RMI 服务
定义一个 RMI 接口

RMI 接口实际上还是一个普通的 Java 接口,只是 RMI 接口必须继承 java.rmi.Remote ,此外,每个 RMI 接口的方法必须声明抛出一个 java.rmi.RemoteException 异常,就像下面这样:

package demo.ZooKeeper.remoting.common;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
    String sayHello(String name) throws RemoteException;
}

继承了 Remote 接口,实际上是让 JVM 得知该接口是需要用于远程调用的,抛出了 RemoteException 是为了让调用 RMI 服务的程序捕获这个异常。毕竟远程调用过程中,什么奇怪的事情都会发生(比如:断网)。需要说明的是,RemoteException 是一个“受检异常”,在调用的时候必须使用 try…catch… 自行处理。

编写 RMI 接口的实现类

实现以上的 HelloService 是一件非常简单的事情,但需要注意的是,我们必须让实现类继承 java.rmi.server.UnicastRemoteObject 类,此外,必须提供一个构造器,并且构造器必须抛出 java.rmi.RemoteException 异常。我们既然使用 JVM 提供的这套 RMI 框架,那么就必须按照这个要求来实现,否则是无法成功发布 RMI 服务的,一句话:我们得按规矩出牌!

package demo.ZooKeeper.remoting.server;

import demo.ZooKeeper.remoting.common.HelloService;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
 
    protected HelloServiceImpl() throws RemoteException {
    }

    @Override

    public String sayHello(String name) throws RemoteException {

        return String.format("Hello %s", name);
    }
}

为了满足 RMI 框架的要求,我们确实做了很多额外的工作(继承了 UnicastRemoteObject 类,抛出了 RemoteException 异常),但这些工作阻止不了我们发布 RMI 服务的决心!我们可以通过 JVM 提供的 JNDI( Java Naming and Directory Interface ,Java 命名与目录接口)这个 API 轻松发布 RMI 服务。

通过 JNDI 发布 RMI 服务

发布 RMI 服务,我们需要告诉 JNDI 三个基本信息:

  1. 域名或 IP 地址(host)、
  2. 端口号(port)、
  3. 服务名(service),
    它们构成了 RMI 协议的 URL(或称为“RMI 地址”):
rmi://<host>:<port>/<service>

如果我们是在本地发布 RMI 服务,那么 host 就是 “ localhost ” 。此外,RMI 默认的 port 是 “ 1099 ”,我们也可以自行设置 port 的值(只要不与其它端口冲突即可)。service 实际上是一个基于同一 host 与 port 下唯一的服务名,我们不妨使用 Java 完全类名来表示吧,这样也比较容易保证 RMI 地址的唯一性。

对于我们的示例而言,RMI 地址为:

rmi://localhost:1099/demo.ZooKeeper.remoting.server.HelloServiceImpl

我们只需简单提供一个 main() 方法就能发布 RMI 服务,就像下面这样:

package demo.ZooKeeper.remoting.server;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
 

public class RmiServer {

    public static void main(String[] args) throws Exception {
        int port = 1099;
        String url = "rmi://localhost:1099/demo.ZooKeeper.remoting.server.HelloServiceImpl";

        LocateRegistry.createRegistry(port);

        Naming.rebind(url, new HelloServiceImpl());

    }
}

需要注意的是,我们通过 LocateRegistry.createRegistry() 方法在 JNDI 中创建一个注册表,只需提供一个 RMI 端口号即可。此外,通过 Naming.rebind() 方法绑定 RMI 地址与 RMI 服务实现类,这里使用了 rebind() 方法,它相当于先后调用 Naming 的 unbind() 与 bind() 方法,只是使用 rebind() 方法来得更加痛快而已,所以我们选择了它。

运行这个 main() 方法,RMI 服务就会自动发布,剩下要做的就是写一个 RMI 客户端来调用已发布的 RMI 服务。

调用 RMI 服务

同样我们也使用一个 main() 方法来调用 RMI 服务,相比发布而言,调用会更加简单,我们只需要知道两个东西:1. RMI 请求路径、2. RMI 接口(一定不需要 RMI 实现类,否则就是本地调用了)。数行代码就能调用刚才发布的 RMI 服务,就像下面这样:

package demo.ZooKeeper.remoting.client;
 

import demo.ZooKeeper.remoting.common.HelloService;

import java.rmi.Naming;
 

public class RmiClient {
 

    public static void main(String[] args) throws Exception {

        String url = "rmi://localhost:1099/demo.ZooKeeper.remoting.server.HelloServiceImpl";

        HelloService helloService = (HelloService) Naming.lookup(url);

        String result = helloService.sayHello("Jack");

        System.out.println(result);

    }
}

当我们运行以上 main() 方法,在控制台中看到 “ Hello Jack ” 输出,就表明 RMI 调用成功。

RMI 服务的局限性

可见,借助 JNDI 这个所谓的命名与目录服务,我们成功地发布并调用了 RMI 服务。实际上,JNDI 就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。在服务端我们发布了 RMI 服务,并在 JNDI 中进行了注册,此时就在服务端创建了一个 Skeleton(骨架),当客户端第一次成功连接 JNDI 并获取远程服务对象后,立马就在本地创建了一个 Stub(存根),远程通信实际上是通过 Skeleton 与 Stub 来完成的,数据是基于 TCP/IP 协议,在“传输层”上发送的。毋庸置疑,理论上 RMI 一定比 WebService 要快,毕竟 WebService 是基于 HTTP 的,而 HTTP 所携带的数据是通过“应用层”来传输的,传输层较应用层更为底层,越底层越快。

既然 RMI 比 WebService 快,使用起来也方便,那么为什么我们有时候还要用 WebService 呢?

其实原因很简单,WebService 可以实现跨语言系统之间的调用,而 RMI 只能实现 Java 系统之间的调用。也就是说,RMI 的跨平台性不如 WebService 好,假如我们的系统都是用 Java 开发的,那么当然首选就是 RMI 服务了。

貌似 RMI 确实挺优秀的,除了不能跨平台以外,还有那些问题呢?

主要有以下两点局限性:

  1. RMI 使用了 Java 默认的序列化方式,对于性能要求比较高的系统,可能需要使用其它序列化方案来解决( 例如:Protobuf )。
  2. RMI 服务在运行时难免会存在出故障,例如,如果 RMI 服务无法连接了,就会导致客户端无法响应的现象。
    在一般的情况下,Java 默认的序列化方式确实已经足以满足我们的要求了,如果性能方面如果不是问题的话,我们需要解决的实际上是第二点,也就是说,让使系统具备 HA(High Availability,高可用性)。
使用 ZooKeeper 提供高可用的 RMI 服务

要想解决 RMI 服务的高可用性问题,我们需要利用 ZooKeeper 充当一个 服务注册表(Service Registry),让多个 服务提供者(Service Provider)形成一个集群,让 服务消费者(Service Consumer)通过服务注册表获取具体的服务访问地址(也就是 RMI 服务地址)去访问具体的服务提供者。如下图所示:
在这里插入图片描述
需要注意的是,服务注册表并不是 Load Balancer(负载均衡器),提供的不是“反向代理”服务,而是“服务注册”与“心跳检测”功能。

在这里插入图片描述
利用服务注册表来注册 RMI 地址,这个很好理解,那么“心跳检测”又如何理解呢?说白了就是通过服务中心定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,只会从还“活着”的服务提供者中选出一个做为当前的服务提供者。

也许服务中心可能会出现单点故障,如果服务注册表都坏掉了,整个系统也就瘫痪了。看来要想实现这个架构,必须保证服务中心也具备高可用性。ZooKeeper 正好能够满足我们上面提到的所有需求。

使用 ZooKeeper 的临时性 ZNode 来存放服务提供者的 RMI 地址,一旦与服务提供者的 Session 中断,会自动清除相应的 ZNode。
让服务消费者去监听这些 ZNode ,一旦发现 ZNode 的数据( RMI 地址)有变化,就会重新获取一份有效数据的拷贝。
ZooKeeper 与生俱来的集群能力(例如:数据同步与领导选举特性),可以确保服务注册表的高可用性。

服务提供者

需要编写一个 ServiceProvider 类,来发布 RMI 服务,并将 RMI 地址注册到 ZooKeeper 中(实际存放在 ZNode 上)。

package demo.ZooKeeper.remoting.server;
 

import demo.ZooKeeper.remoting.common.Constant;

import java.io.IOException;

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.Remote;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

import java.util.concurrent.CountDownLatch;

import org.apache.ZooKeeper.CreateMode;

import org.apache.ZooKeeper.KeeperException;

import org.apache.ZooKeeper.WatchedEvent;

import org.apache.ZooKeeper.Watcher;

import org.apache.ZooKeeper.ZooDefs;

import org.apache.ZooKeeper.ZooKeeper;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;
 

public class ServiceProvider {
 

    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceProvider.class);
 

    // 用于等待 SyncConnected 事件触发后继续执行当前线程

    private CountDownLatch latch = new CountDownLatch(1);
 

    // 发布 RMI 服务并注册 RMI 地址到 ZooKeeper 中

    public void publish(Remote remote, String host, int port) {

        String url = publishService(remote, host, port); // 发布 RMI 服务并返回 RMI 地址

        if (url != null) {

            ZooKeeper zk = connectServer(); // 连接 ZooKeeper 服务器并获取 ZooKeeper 对象

            if (zk != null) {

                createNode(zk, url); // 创建 ZNode 并将 RMI 地址放入 ZNode 上

            }

        }

    }
 

    // 发布 RMI 服务

    private String publishService(Remote remote, String host, int port) {

        String url = null;

        try {

            url = String.format("rmi://%s:%d/%s", host, port, remote.getClass().getName());

            LocateRegistry.createRegistry(port);

            Naming.rebind(url, remote);

            LOGGER.debug("publish rmi service (url: {})", url);

        } catch (RemoteException | MalformedURLException e) {

            LOGGER.error("", e);

        }

        return url;

    }
 

    // 连接 ZooKeeper 服务器

    private ZooKeeper connectServer() {

        ZooKeeper zk = null;

        try {

            zk = new ZooKeeper(Constant.ZK_CONNECTION_STRING, Constant.ZK_SESSION_TIMEOUT, new Watcher() {

                @Override

                public void process(WatchedEvent event) {

                    if (event.getState() == Event.KeeperState.SyncConnected) {

                        latch.countDown(); // 唤醒当前正在执行的线程

                    }

                }

            });

            latch.await(); // 使当前线程处于等待状态

        } catch (IOException | InterruptedException e) {

            LOGGER.error("", e);

        }

        return zk;

    }
 

    // 创建 ZNode

    private void createNode(ZooKeeper zk, String url) {

        try {

            byte[] data = url.getBytes();

            String path = zk.create(Constant.ZK_PROVIDER_PATH, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 创建一个临时性且有序的 ZNode

            LOGGER.debug("create ZooKeeper node ({} => {})", path, url);

        } catch (KeeperException | InterruptedException e) {

            LOGGER.error("", e);

        }

    }
}

涉及到的 Constant 常量,见如下代码:

package demo.ZooKeeper.remoting.common;
 

public interface Constant {
 

    String ZK_CONNECTION_STRING = "192.168.20.52:2181,192.168.20.53:2181,192.168.20.54:2181";

    int ZK_SESSION_TIMEOUT = 5000;

    String ZK_REGISTRY_PATH = "/registry";

    String ZK_PROVIDER_PATH = ZK_REGISTRY_PATH + "/provider";
}

注意:我们首先需要使用 ZooKeeper 的客户端工具创建一个持久性 ZNode,名为 “/registry” ,该节点是不存放任何数据的,可使用如下命令:

create /registry null
服务消费者

服务消费者需要在创建的时候连接 ZooKeeper ,同时监听 /registry 节点的 NodeChildrenChanged 事件,也就是说,一旦该节点的子节点有变化,就需要重新获取最新的子节点。这里提到的子节点,就是存放服务提供者发布的 RMI 地址。需要强调的是,这些子节点都是临时性的,当服务提供者与 ZooKeeper 服务注册表的 Session 中断后,该临时性节会被自动删除。

package demo.ZooKeeper.remoting.client;
 

import demo.ZooKeeper.remoting.common.Constant;

import java.io.IOException;

import java.net.MalformedURLException;

import java.rmi.ConnectException;

import java.rmi.Naming;

import java.rmi.NotBoundException;

import java.rmi.Remote;

import java.rmi.RemoteException;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.ThreadLocalRandom;

import org.apache.ZooKeeper.KeeperException;

import org.apache.ZooKeeper.WatchedEvent;

import org.apache.ZooKeeper.Watcher;

import org.apache.ZooKeeper.ZooKeeper;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;
 

public class ServiceConsumer {
 

    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceConsumer.class);
 

    // 用于等待 SyncConnected 事件触发后继续执行当前线程

    private CountDownLatch latch = new CountDownLatch(1);
 

    // 定义一个 volatile 成员变量,用于保存最新的 RMI 地址(考虑到该变量或许会被其它线程所修改,一旦修改后,该变量的值会影响到所有线程)

    private volatile List<String> urlList = new ArrayList<>(); 
 

    // 构造器

    public ServiceConsumer() {

        ZooKeeper zk = connectServer(); // 连接 ZooKeeper 服务器并获取 ZooKeeper 对象

        if (zk != null) {

            watchNode(zk); // 观察 /registry 节点的所有子节点并更新 urlList 成员变量

        }

    }
 

    // 查找 RMI 服务

    public <T extends Remote> T lookup() {

        T service = null;

        int size = urlList.size();

        if (size > 0) {

            String url;

            if (size == 1) {

                url = urlList.get(0); // 若 urlList 中只有一个元素,则直接获取该元素

                LOGGER.debug("using only url: {}", url);

            } else {

                url = urlList.get(ThreadLocalRandom.current().nextInt(size)); // 若 urlList 中存在多个元素,则随机获取一个元素

                LOGGER.debug("using random url: {}", url);

            }

            service = lookupService(url); // 从 JNDI 中查找 RMI 服务

        }

        return service;

    }
 

    // 连接 ZooKeeper 服务器

    private ZooKeeper connectServer() {

        ZooKeeper zk = null;

        try {

            zk = new ZooKeeper(Constant.ZK_CONNECTION_STRING, Constant.ZK_SESSION_TIMEOUT, new Watcher() {

                @Override

                public void process(WatchedEvent event) {

                    if (event.getState() == Event.KeeperState.SyncConnected) {

                        latch.countDown(); // 唤醒当前正在执行的线程

                    }

                }

            });

            latch.await(); // 使当前线程处于等待状态

        } catch (IOException | InterruptedException e) {

            LOGGER.error("", e);

        }

        return zk;

    }
 

    // 观察 /registry 节点下所有子节点是否有变化

    private void watchNode(final ZooKeeper zk) {

        try {

            List<String> nodeList = zk.getChildren(Constant.ZK_REGISTRY_PATH, new Watcher() {

                @Override

                public void process(WatchedEvent event) {

                    if (event.getType() == Event.EventType.NodeChildrenChanged) {

                        watchNode(zk); // 若子节点有变化,则重新调用该方法(为了获取最新子节点中的数据)

                    }

                }

            });

            List<String> dataList = new ArrayList<>(); // 用于存放 /registry 所有子节点中的数据

            for (String node : nodeList) {

                byte[] data = zk.getData(Constant.ZK_REGISTRY_PATH + "/" + node, false, null); // 获取 /registry 的子节点中的数据

                dataList.add(new String(data));

            }

            LOGGER.debug("node data: {}", dataList);

            urlList = dataList; // 更新最新的 RMI 地址

        } catch (KeeperException | InterruptedException e) {

            LOGGER.error("", e);

        }

    }
 

    // 在 JNDI 中查找 RMI 远程服务对象

    @SuppressWarnings("unchecked")

    private <T> T lookupService(String url) {

        T remote = null;

        try {

            remote = (T) Naming.lookup(url);

        } catch (NotBoundException | MalformedURLException | RemoteException e) {

            if (e instanceof ConnectException) {

                // 若连接中断,则使用 urlList 中第一个 RMI 地址来查找(这是一种简单的重试方式,确保不会抛出异常)

                LOGGER.error("ConnectException -> url: {}", url);

                if (urlList.size() != 0) {

                    url = urlList.get(0);

                    return lookupService(url);

                }

            }

            LOGGER.error("", e);

        }

        return remote;

    }
}
发布服务

我们需要调用 ServiceProvider 的 publish() 方法来发布 RMI 服务,发布成功后也会自动在 ZooKeeper 中注册 RMI 地址。

package demo.ZooKeeper.remoting.server;
 

import demo.ZooKeeper.remoting.common.HelloService;
 

public class Server {
 

    public static void main(String[] args) throws Exception {

        if (args.length != 2) {

            System.err.println("please using command: java Server <rmi_host> <rmi_port>");

            System.exit(-1);

        }
 

        String host = args[0];

        int port = Integer.parseInt(args[1]);
 

        ServiceProvider provider = new ServiceProvider();
 

        HelloService helloService = new HelloServiceImpl();

        provider.publish(helloService, host, port);
 

        Thread.sleep(Long.MAX_VALUE);

    }
}

注意:在运行 Server 类的 main() 方法时,一定要使用命令行参数来指定 host 与 port ,例如:

java Server localhost 1099

java Server localhost 2099

以上两条 Java 命令可在本地运行两个 Server 程序,当然也可以同时运行更多的 Server 程序,只要 port 不同就行。

调用服务

通过调用 ServiceConsumer 的 lookup() 方法来查找 RMI 远程服务对象。我们使用一个“死循环”来模拟每隔 3 秒钟调用一次远程方法。

package demo.ZooKeeper.remoting.client;
 

import demo.ZooKeeper.remoting.common.HelloService;
 

public class Client {
 

    public static void main(String[] args) throws Exception {

        ServiceConsumer consumer = new ServiceConsumer();
 

        while (true) {

            HelloService helloService = consumer.lookup();

            String result = helloService.sayHello("Jack");

            System.out.println(result);

            Thread.sleep(3000);

        }

    }
}
使用方法

根据以下步骤验证 RMI 服务的高可用性:
运行两个 Server 程序,一定要确保 port 是不同的。
运行一个 Client 程序。
停止其中一个 Server 程序,并观察 Client 控制台的变化(停止一个 Server 不会导致 Client 端调用失败)。
重新启动刚才关闭的 Server 程序,继续观察 Client 控制台变化(新启动的 Server 会加入候选)。
先后停止所有的 Server 程序,还是观察 Client 控制台变化(Client 会重试连接,多次连接失败后,自动关闭)。

总结

通过使用 ZooKeeper 实现了一个简单的 RMI 服务高可用性解决方案,通过 ZooKeeper 注册所有服务提供者发布的 RMI 服务,让服务消费者监听 ZooKeeper 的 Znode,从而获取当前可用的 RMI 服务。此方案局限于 RMI 服务,对于任何形式的服务(比如:WebService),也提供了一定参考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值