理论
docker部署
1.下载zookeeper 最新版镜像
docker search zookeeper
docker pull zookeeper
docker images
- 在windows下d盘中创建一个文件夹D:\dockercontainers\zookeeper。
d:
mkdir dockercontainers
cd dockercontainers
mkdir zookeeper
- 启动服务
docker run -d -e TZ="Asia/Shanghai" -p 2181:2181 -v d:\dockercontainers\zookeeper:/data --name zookeeper --restart always zookeeper
-e TZ="Asia/Shanghai" # 指定上海时区
-d # 表示在一直在后台运行容器
-p 2181:2181 # 对端口进行映射,将本地2181端口映射到容器内部的2181端口
--name # 设置创建的容器名称
-v # 将本地目录(文件)挂载到容器指定目录;
--restart always #始终重新启动zookeeper
- 进入容器
docker exec -it zookeeper /bin/bash
- 启动Zookeeper客户端
zkCLi.sh
基本命令
help 显示所有操作命令
ls path 使用 ls 命令来查看当前 znode 的子节点 [可监听]
-w 监听子节点变化
-s 附加次级信息
create 普通创建(临时节点不能创建字节点)
-s 含有序列
-e 临时(重启或者超时消失)
get path 获得节点的值 [可监听]
-w 监听节点内容变化
-s 附加次级信息
set 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点
docker集群部署
- 在docker中创建zoonet网桥。
docker network ls
docker network create --driver bridge --subnet=172.19.0.0/16 zoonet
--subnet=172.19.0.0/16 子网段,不能与其他网桥冲突
zoonet 网桥名
- 创建三个容器,映射到本机不同的端口,挂载到本地不同的目录( data, logs),设置三个容器中运行的zk的不同的服务id, 并配置服务器集群配置。
docker run -d -p 2181:2181 --name zookeeper_node1 --privileged --restart always --network zoonet --ip 172.19.0.2 -v D:\dockercontainers\zkcluster\node1\volumes\data:/data -v D:\dockercontainers\zkcluster\node1\volumes\datalog:/datalog -v D:\dockercontainers\zkcluster\node1\volumes\logs:/logs -e ZOO_MY_ID=1 -e "ZOO_SERVERS=server.1=172.19.0.2:2888:3888;2181 server.2=172.19.0.3:2888:3888;2181 server.3=172.19.0.4:2888:3888;2181" 36c607e7b14d
docker run -d -p 2182:2181 --name zookeeper_node2 --privileged --restart always --network zoonet --ip 172.19.0.3 -v D:\dockercontainers\zkcluster\node2\volumes\data:/data -v D:\dockercontainers\zkcluster\node2\volumes\datalog:/datalog -v D:\dockercontainers\zkcluster\node2\volumes\logs:/logs -e ZOO_MY_ID=2 -e "ZOO_SERVERS=server.1=172.19.0.2:2888:3888;2181 server.2=172.19.0.3:2888:3888;2181 server.3=172.19.0.4:2888:3888;2181" 36c607e7b14d
docker run -d -p 2183:2181 --name zookeeper_node3 --privileged --restart always --network zoonet --ip 172.19.0.4 -v D:\dockercontainers\zkcluster\node3\volumes\data:/data -v D:\dockercontainers\zkcluster\node3\volumes\datalog:/datalog -v D:\dockercontainers\zkcluster\node3\volumes\logs:/logs -e ZOO_MY_ID=3 -e "ZOO_SERVERS=server.1=172.19.0.2:2888:3888;2181 server.2=172.19.0.3:2888:3888;2181 server.3=172.19.0.4:2888:3888;2181" 36c607e7b14d
2181:是zk客户端联接端口
2888:选举leader的端口
3888:集群内服务器通讯端口.
- 查看每台主机的状态
docker exec -it zookeeper_node1 /bin/bash
zkServer.sh status
Zookeeper理论
Zookeeper是什么
1. 分布式应用程序协调服务,分布式应用提供一致性服务的软件。
2. zookeeper=文件系统+通知机制
2.1文件系统
Zookeeper维护一个类似文件系统的数据结构:每个子目录项都被称作为 znode,和文 件系统一样,我们能够自由的增加、删除znode,在一个znode下(持久节点)增加、删除 子znode,唯一的不同在于znode是可以存储数据的;
它有四种节点类型:顺序持久节点,顺序临时节点,持久节点,临时节点。
2.2 通知机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据变化、子节点变化)时,zookeeper会通知客户端。
能做什么
Zookeeper 的命名服务(文件系统)
Zookeeper 的配置管理(文件系统、通知机制)
Zookeeper 集群管理(文件系统、通知机制)
Zookeeper 分布式锁(文件系统、通知机制)
Zookeeper 队列管理(文件系统、通知机制)
...
如何实现数据的一致性服务
Paxos 算法
工作原理
基本概念
角色:
领导者(Leader):负责进行投票的发起和决议,更新系统状态;
学习者(Learner):
跟随者(Follower):用于接受客户端请求并向客户端返回结果,在选举过程中参与投票;
观察者(ObServer):可以接收客户端请求,将写请求转发给Leader,但不参与投票过程,只同步Leader状态。
目的:扩展系统,提高读取速度;
客户端(Client):请求发起方;
设计目的:
1)zookeeper是顺序一致性,这是一种强一致性,最终一致性模型是非常弱的一致性模型。可以这么说,满足顺序一致性的系统一定满足最终一致性,但满足最终一致性的系统不一定满足顺序一致性。
2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以Zookeeper适合安装奇数台服务器。
3)全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4)更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
5)数据更新原子性,一次数据更新要么成功,要么失败。
6)实时性,在一定时间范围内,Client能读到最新数据。
7)等待无关,慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
当设计 ZooKeeper 的 API 的时候,我们抛弃了阻塞原语,例如 Locks。
阻塞原语对于一个协调服务会引起其它问题,速度较慢或者故障的客户端会对速度较快的客户端产生负面的影响。
如果处理请求取决于客户端的响应和其他客户端的失败检测,则服务本身的实现将变得更加复杂。
因此,我们的系统 ZooKeeper 实现了一个API,该 API 可以按文件系统的层次结构来组织简单的 wait-free 数据对象。
实际上,ZooKeeper API类似于任何其他文件系统,并且仅从 API 签名来看,ZooKeeper 很像没有锁定(lock)方法,打开(open)和关闭(close)方法的 Chubby。
但是,实现 wait-free 数据对象使 ZooKeeper 与基于锁之类的阻塞原语的系统明显不同。
尽管 wait-free 对于性能和容错性很重要,但不足以进行协调。我们还必须提供操作的顺序保证。
特殊的,我们发现,对客户端所有操作提供 FIFO 语义与提供 linearizable writes 可以高效的实现服务,并且足以实现应用程序感兴趣的协调原语。
原理
Zookeeper的核心是事件原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。
Zab协议
Zookeeper应用 代码
工具类
- 节点信息输出
- 获得Zookeeper连接:zookeeper连接需要一定时间,为保证后面的代码使用的zookeeper对象是已连接上的,所以使用CountDownLatch来进行控制;
zkClient = new ZooKeeper(connectString, sessionTimeout, (event) -> {
//收到事件通知后的回调函数(业务逻辑)
System.out.println("事件类型:" + event.getType() + " 服务器状态:" + event.getState() + " 事件发送的节点路径:" + event.getPath());
//只有状态是连接上时,才激活判断
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
// System.out.println("zk客户端与服务器建立连接");
countDown.countDown();//建立连接了再释放锁,让主程序运行
}
});
countDown.await();
基本方法
- 创建节点:
//ZooDefs.Ids.OPEN_ACL_UNSAFE通过移位和或运算来实现权限控制
//参数:节点路径、节点数据、节点权限、节点类型:持久,持久有序,临时,临时有序
zk.create(path, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
- 删除节点:delete(路径,预期节点版本)
- 判断节点是否存在:exists(路径,监听)
- 获得节点信息:getData(路径,监听,start)
统一配置管理
知识点
zookeeper
1. 持久节点的创建;
2. 节点数据变化的事件监听;
3. 安全认证;
spring
- 生命周期的管理:
1.1 @DependsOn:强制依赖,此类的对象必须先被 spring 托管。 - 在托管类中注入spring容器(ApplicationContextAware接口)。
- druid数据源的刷新。
DruidDataSource ds= (DruidDataSource)ac.getBean( "getDataSource");
ds.restart(); // *********重启数据源
实现步骤
- AddZkConfigMessage:向zookeeper中创建节点并存入配置信息;
1.1 用户输入zookeeper的url、超时时间、配置的起始节点路径、安全认证类型、安全认证密码、配置信息的节点名和内容(Map)节点名和设置数据源的set的属性名相同。
1.2 先判断是否有这些节点,如果有则删除,然后再创建并写入内容; - MyClient implements Watcher, ApplicationContextAware:
2.1 设置事件监听,主要是Event.EventType.NodeDataChanged节点信息的更新,触发则刷新数据源。
2.2 用户输入:需要访问配置的起始节点数据名、设置数据源配置的bean
2.3 获取数据:递归遍历起始节点的子节点,如果到一个节点没有子节点则取出这个节点的数据,并通过反射将数据通过bean的set方法注入bean中。 - DruidConfig:与springBoot整合。
- TestMain:springBoot启动类。
统一集群管理
知识点
- 临时节点创建:一个master节点,多client节点;
- 节点监听:监听master节点的删除事件;
- master节点的选举;
- master节点的抢占优化;
- 节点信息保存使用对象流,对信息的序列化和反序列化;
实现步骤
- RunningData:将要保持的数据封装成一个对象,利用Record接口对对象中的属性进行序列化和反序列化处理,Record接口可以可以控制元素的写入顺序,可以处理图片等比implements Serializable 处理更加灵活。
- WorkServer:控制服务上下线时,注册进zookeeper和对master节点的争抢。
2.1 判断服务器是否启动,未启动,则在zookeeper的servers下注册自己(创建临时节点),并利用zk的getData方法绑定对master节点的监听,如果master节点不存在则捕获错误,让当前节点去抢占master节点。
2.2 如果master掉线则触发事件监听,所有服务去抢占master节点,节点触发监听事件后,比较节点名和保存的master节点名,如果一样则直接抢占master节点,不一样则利用定时任务延时5s后再争抢master(优先上次master节点争抢,防止出现网络抖动等情况导致master掉线)。 - LeaderSelectorZkClient:测试类:模拟上线后有多个workServer,和master五秒掉线的情况。
队列实现(生产者消费者模式)
知识点
- 持久顺序节点;
- 阻塞和非阻塞;
- 先进先出(FIFO)原则;
实现步骤
- SimpleDistributedQueue非阻塞队列:
1.1 向队列中存入数据offer(T element) :先拼接节点路径,然后将传入的对象转化为对象流的形式,创建节点并写入数据。
1.2 从队列中取出数据pull():获得root节点的所有子节点集合,然后对子节点进行从小到大排序(顺序节点),取出排序后的第一个节点(节点顺序最小,最先进入队列的),然后拼接节点路径,读取出这个节点的数据并删除这个节点。 - DistributedBlockQueue阻塞队列,继承SimpleDistributedQueue非阻塞队列:
2.1 因为理论上以zookeeper实现的队列是无界队列(zookeeper的机器无限扩充),所以阻塞和非阻塞队列的添加操作相同,只需重新pull()方法就行了。
2.2 利用CountDownLatch来阻塞线程;
2.3 创建一个监听事件,监听root节点的子节点的变化;
2.4 设置一个while循环,通过zk的getChildren方法来对root节点绑定事件监听,调用非阻塞队列的pull()方法获取数据,如果pull()方法获取到数据则返回,如果没有获得数据则阻塞线程,直到有线程offer(T element) 存入数据触发监听,才解除阻塞重新获取数据。 - Order:待存数据;
- 测试代码:
4.1 Test1:以自定义的队列实现生产者消费者
4.2 Test2:SimpleDistributedQueue测试offer(T element) 和pull()方法;
4.3 Test3:DistributedBlockQueue测试offer(T element) 和pull()方法;
分布式锁实现(公平锁)
实现方式
- 使用数据库,阻塞式的行锁 Select * from xx for updata;
Updata xxxx set ticks=ticks_1
效率慢,单点故障(数据库挂了,集群配置困难); - 用redis
Get lock
Setnx lock ip… 超时时间,超时时间不好控制)
用redisson 实现,超时时间控制(看门狗) - 用zookeeper实现:
独占,非公平:就是所有试图来获取这个锁(znode)的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
公平: 1. 客户端在/locks根节点下面创建临时有序节点(这个可以通过节点的属性控制CreateMode.EPHEMERAL_SEQUENTIAL来指 定),
2.client调用getChildren(“/root/lock_”,watch)来获取所有已经创建的子节点,并同时在这个节点上注册子节点变更通知的Watcher。
知识点
- 创建临时顺序节点;
- 每个节点监听前一个节点的删除事件;
- redis,存储票数;
- nginx,实现多个服务之间的负载均衡;
- curator框架;
- ab测试;
抢票程序三个版本
- 版本一: 单机锁: 采用 ReentrantLock实现。
- 版本二: 利用nginx完成服务集群的搭建, 并实现了nginx的负载均衡。解决的问题: 高并发场景下单服务器的处理能力不够的问题。利用ab测试模拟多个用户( 1000个用户 ) 密集并发的场景。
ab.exe -n 100 -c 6 http://distributedlock/ticket2
产生问题: 单机锁无法保证在服务集群下数据操作的安全性。存在一票多卖的情况,但单机上不会出现重复的票。
解决方案:自定义基于zookeeper的分布式锁。来解决分布式集群环境下单机锁失效的问题。 - 版本三:使用curator框架,解决连接重连、反复注册Watcher和NodeExistsException等情况。
自定义分布式锁实现步骤
1. 使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/locks目录下。
2. 创建节点成功后,获取/locks目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点
3. 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
4. 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听(不是监听最前面的那个节点防止羊群效应)。
比如当前线程获取到的节点序号为/locks/seq-00000003,然后所有的节点列表为[/locks/seq-00000001,/locks/seq-00000002,/locks/seq-00000003],则对/locks/seq-00000002这个节点添加一个事件监听器。
5. 如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。
比如/locks/seq-00000001释放了,/locks/seq-00000002监听到事件,此时节点集合为[/locks/seq-00000002,/locks/seq-00000003],则/locks/seq-00000002为最小序号节点,获取到锁。