一、初始Zookeeper
概念:
- :blue_book:
zookeeper是Apache Hadoop项目下的一个子项目,是一个分布式协调服务用于管理分布式应用程序,翻译过来就是动物管理员,其目录结构是同Linux的目录结构一样是一个树形结构。 - :blue_book:
zookeeper的主要用来干嘛:- :book: 配置管理
- :book: 分布式锁
- :book: 集群管理
- :book: 注册中心
下载: :link: zookeeper.apache.org/releases.ht…
安装:
-
:closed_book: 安装单机版:
- :one: 安装
zookeeper的前提需要安装JDK, - :link: www.runoob.com/w3cnote/win…
- :two: 将下载好的安装包上传到
linux服务器,如果你使用的远程连接工具是FinalShell,那么你可以点击底栏的倒数第二个图标就可以上传文件。 - :three: 解压到指定的目录即可:
tar -zxvf 安装包 -C /path - :four: 解压完成后需要进入
conf目录,修改配置文件mv zoo_sample.cfg zoo.cfg注意,一定要修改为zoo.cfg否则会找不到该文件。 - :five: 然后进入
zoo.cfg文件,修改文件临时存储的位置为自己指定的位置,这里我指定在bin目录同级
- :one: 安装
-
-
:six: 最后分别启动服务端和客户端:
./zkServer.sh start./zkCli.sh
zoo.cfg配置文件参数说明:
- :shamrock:
tickTime=2000通信心跳时间,是指zookeeper的服务端和客户端之间的通信心跳时间,如果超过了这个时间就会认为其中的一端挂掉了。 - :shamrock:
initLimit=10领导者和追随者之间初次通信的时间限制次数,这里便是initLimit * tickTime也就是说这里最多会等待20秒。 - :shamrock:
syncLimit=5领导者和追随者之间数据的同步时间限制次数,如果Leader认为Follwer超过了syncLimit * tickTime那么,Leader就会认为Follwer已经挂掉,就会删除改节点。 - :shamrock:
dataDir安装时修改的配置文件路劲,默认是tmp路径下,而默认路径在一段时间后就会自动清理掉,所以我们会更改文件存储的路径。 - :shamrock:
clientPort客户端的端口
zookeeper集群搭建:
:nail_care: PS: 搭建zk集群至少3台服务器,集群数量达到一半就可以正常运行,搭建集群时最好是奇数台服务器这样能更好的体现zk的性能。
-
:one: 在安装单机版的基础上,创建一个名为
myid的文件夹放在zoo.cfg中的dataDir路径下,而且该文件必须叫myid,在该文件中添加一个身份标识,如IP: 192.168.23.121,就需要在该文件中输入1,以此类推。 -
:two: 在
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: 表示这是第几号服务器,需要和
- 参数说明:
-
:three: 启动集群:
./zkServer.sh start,查看状态./zkServer.sh status如果你现在有三台服务器,那么当你启动第一台服务器时会出现ERROR的提示,那是因为集群的数量还没有达到一半,所以你只需要再启动一台就不会报错了。
二、Zookeeper 命令操作:
:m:zk 的数据模型:
- :book: 拥有树形目录的
zk是一个很有层次化的结构
- :book:
zk中的每一个节点被称为:ZNode,每个节点都会保存自己的数据和节点信息,同时也是允许保存少量内容(1MB)在该节点下。 - :book: 分类:
- :lemon:
persistent持久化节点 - :lemon:
ephemeral临时节点 : -e - :lemon:
persistent_sequential持久化顺序节点: -s - :lemon:
ephemeral_sequential临时顺序节点: -es
- :lemon:
:m: 服务端常用命令:
- :book: 启动
zk服务,./zkServer.sh start - :book: 查看
zk状态,./zkServer.sh status - :book:停止
zk服务,./zkServer.sh stop - :book:重启
zk服务,./zkServer.sh restart
:m: 客户端常用命令:
- :book: 连接
zk服务端,./zkCli.sh -server ip:port - :book: 断开连接,
quit - :book: 设置节点值,
set /节点path value - :book: 删除单个节点,
delete /节点path - :book:删除带有子节点的节点,
deleteall /节点path - :book:显示指定目录下的节点,
ls 路径 - :book:创建节点,
create /节点path value - :book:创建临时节点,
create -e /节点path value - :book:创建顺序节点,
create -s /节点path value - :book:创建临时顺序节点,
create -es /节点path value - :book: 获得节点值,
get /节点path - :book: 获得帮助,
help - :book: 查看节点详情,
ls -s /节点path- :maple_leaf:
cZxid节点被创建的事物ID - :maple_leaf:
ctime创建时间 - :maple_leaf:
mZxid最后一次被更新的事物ID - :maple_leaf:
mtime修改时间 - :maple_leaf:
pZxid子节点列表最后一次被更新的事物ID - :maple_leaf:
cversion子节点的版本号 - :maple_leaf:
dataversion数据版本号 - :maple_leaf:
aclversion权限版本号 - :maple_leaf:
ephemeralOwner用于临时节点,代表临时节点是事物ID,如果为持久节点那么ID为0 - :maple_leaf:
dataLength节点存储数据的长度 - :maple_leaf:
numChildren当前节点的子节点个数
- :maple_leaf:
三、JavaAPI 操作:
zk 客户端库的介绍:
-
:book: 原生
JavaAPI,最难用 -
:book: ZKClient,比原生的好点
-
:book:Curator,相比前两种最好的,是Netfix公司研发的后来捐给了
Apache基金会,目前是Apache基金会的顶级项目
Curator 常用API的操作:
-
:book: 建立连接:
-
:one: 导入依赖
<!--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> -
:two: 日志文件:
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 -
:three: : 建立连接:
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端口
-
-
:book: 添加节点:
-
@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()); }
-
-
:book: 删除节点:
-
@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"); }
-
-
:book: 修改节点:
-
@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()); }
-
-
:book: 查询节点:
-
@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: 如果你想要获取顺序节点的内容那么你需要进行序列化
-
-
:book:
Watch事件监听: -
zk可以允许用户在指定节点上注册一些监听器(Watcher), 并且在触发特定事件时通知其它节点,这一机制就是zk中实现分布式协调服务的重要特性。 -
原生的API 操作监听器十分不方便,故此有了
Curator,Curator中使用Cache来实现对zk服务端事件的监听。 -
种类:
- :lemon:
NodeCache: 只监听某个特定的节点 - :lemon:
PathChildrenCache: 监听一个节点下的子节点 - :lemon:
TreeCache: 监听整个树上的所以节点
- :lemon:
-
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){} } -
:book: 分布式锁的实现:
在进行单机应用开发时在对数据并发同步时往往都是采用
syncchronized或者Lock进行加锁的方式取解决数据同步问题,但是在跨机器时的数据同步问题采用这种方式就不可以了,这时就需要使用分布式锁来实现数据同步。
-
锁的分类:
- :sunflower:
InterProcessSemaphoreMutex: 分布式非可重入锁 - :sunflower:
InterProcessMutex: 分布式可重入锁 - :sunflower:
InterProcessReadWriteLock: 分布式读写锁 - :sunflower:
InterProcessMultiLock: 多锁单用 - :sunflower:
InterProcessSemaphoreV2: 共享信号量
- :sunflower:
-
实现:
- 模拟买票:
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,此处假设五台机器
-
:raised_hand: 首次启动时选举:
- 每台机器都有选举自己的一票,当第一次启动时,第一台机器选举自己此时它只有一票未达到总的一半以上便不是
Leader - 第二台机器启动,进行选举,此时会根据
myid文件中的number进行比较大小,值小的将自己的票给值大的,假设第二台机器的值大于第一台,此时第二台拥有两票,未达到一半以上不是Leader - 第三台机器启动,进行选举,假设第三台的
myid中的number大于第二台中myid的number那么第二台机器就会将手中的两票投给第三台机器,此时第三台机器则拥有1+2的票数,达到一半以上则为Leader,如果一旦确认了Leader则其它机器便默认为follwer。
- 每台机器都有选举自己的一票,当第一次启动时,第一台机器选举自己此时它只有一票未达到总的一半以上便不是
-
:raised_hand: 二次选举:
- 此时假设第五台机器无法和
Leader保持通信了,开始第二次选举。 - 选举时的两种情况:
Leader任然存在- 此时第五台机器会进行重试与
Leader进行通信,如果能通信成功则与Leader进行同步,如果不能成功则默认Leader也挂了。
- 此时第五台机器会进行重试与
Leader已经挂了- 这时第三台机器和第五台机器已经挂了,这
1,2,4进行选举,选举的依据是:【Epoch(领导者任期编号) 、ZXID(事物ID) 、SID(服务器ID)】,根据Epoch > ZXID > SID的规则进行选举。
- 这时第三台机器和第五台机器已经挂了,这
- 此时假设第五台机器无法和
zk 中的分布式锁原理:
- :book: 核心思想:客户端想要获得锁,那么就需要创建节点,使用完锁,删除节点。
- :one: 当客户端想要获得锁时会在对应节点创建一个临时顺序节点,使用完了以后删除该节点。
- :two: 然后会获得该节点下的所有子节点,根据节点顺序值比较大小,最小的则会获得到锁,如果不是最小的则大的节点会在自己的前一个节点注册一个监听器,用来监听删除事件。
- :three: 如果发现前一个节点触发了删除事件,那么会再一次进行比较谁的值最小,由最小值的节点获得锁。