java zookeeper 使用场景_中间件 - ZooKeeper应用场景实践

注:该文章用作回顾记录

一、准备工作

预先下载安装 ZooKeeper ,简单配置就能使用了。然后构建 Maven 项目,将下面的代码粘贴到 pom.xml中:

org.apache.zookeeper

zookeeper

3.4.5

com.101tec

zkclient

0.5

zkclient 是开源的客户端工具,其中封装了很多功能,比如:删除包含子节点的父节点,连接重试,异步回调,偏向 Java 写法的注册监听等,极大地方便了用户使用。

下面不过多介绍客户端操作,只针对应用场景做介绍,该文章会随着本人的学习持续补充。

二、数据发布/订阅

使用 ZooKeeper 节点监听来实现该功能:

ZkClient zkClient = new ZkClient("IP1:Port1,IP2:Port2,IP3:Port3", 5000); // 连接集群

zkClient.createPersistent("/xxx/xxx"); // 创建持久节点

// 注册子节点变更监听,当子节点改变(比如创建了"/xxx/xxx/1")或当前节点删除等,会触发异步回调

zkClient.subscribeChildChanges("/xxx/xxx", new IZkChildListener() {

@Override

public void handleChildChange(String parentPath, List currentChilds)

throws Exception {

}

});

下面为部分源码:

package org.I0Itec.zkclient;

public class ZkClient implements Watcher {

public List watchForChilds(final String path) {

return retryUntilConnected(new Callable>() {

@Override

public List call() throws Exception {

exists(path, true);

try {

return getChildren(path, true);

} catch (ZkNoNodeException e) {

}

return null;

}

});

}

public T retryUntilConnected(Callable callable) throws ZkInterruptedException, IllegalArgumentException, ZkException, RuntimeException {

final long operationStartTime = System.currentTimeMillis();

while (true) {

if (_closed) {

throw new IllegalStateException("ZkClient already closed!");

}

try {

return callable.call();

} catch (Exception e) {

throw ExceptionUtil.convertToRuntimeException(e);

}

}

}

}

基于 ZooKeeper 实现的数据发布/订阅很简单吧,快动手试试。

三、分布式锁

这部分是 ZooKeeper 重要功能,在此基础上实现诸如,分布式协调/通知,负载均衡,Master选举等复杂场景。

1、排它锁

排它锁又称为写锁或独占锁。比如事务 T1 对数据对象 O1 加了排它锁,那么在整个加锁期间,只允许 T1 对 O1 进行读取或更新操作,其它事务都不能对 O1 操作。

1)获取锁

所有客户端都创建临时节点 zkClient.createEphemeral("/xxx/xxx", null);,ZooKeeper 会保证在所有客户端中,最终只有一个客户端能创建成功,那么就认为该客户端获取了锁。同时,所有没获取到锁的客户端需在/xxx/xxx 上注册子节点变更监听,以便实时监听节点变化。如节点发生变化,则未获取到锁的客户端再重新获取锁。

private static ZkClient zkClient = new ZkClient("IP1:Port1,IP2:Port2,IP3:Port3", 5000);

private static final String lockParentPath = "/zk-book/exclusice_lock";

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

try {

zkClient.createEphemeral(lockParentPath + "/lock");

System.out.println("service3 获取锁成功");

} catch (Exception e) {

System.out.println("service3获取锁失败");

zkClient.subscribeChildChanges(lockParentPath, new IZkChildListener() {

@Override

public void handleChildChange(String parentPath, List currentChilds)

throws Exception {

System.out.println("service3再次获取锁");

main(null);

}

});

}

Thread.sleep(Integer.MAX_VALUE);

}

2)释放锁

当 "/xxx/xxx" 是临时节点时,以下俩种情况都会释放锁。

当已获取锁的客户机宕机,导致连接超时断开,那么 ZooKeeper 会将临时节点删除。

正常执行完逻辑后,客户端主动将临时节点删除。

574102048aac6ea580722f04d4006bb7.png

2、共享锁

共享锁又称为读锁。如果事务 T1 对数据对象 O1 加了共享锁,那么 T1 只能对 O1 进行读取操作,其它事务只能对 O1 加共享锁,直到 O1 上所有共享锁都被释放。

1)获取锁

所有客户端都创建临时顺序节点 zkClient.createEphemeralSequential("/xxx/xxx", null);,ZooKeeper 会生成类似下面的节点,已保证节点的唯一性。

da873f58dba094aae5ca3a23ac27141f.png

2)判断读写顺序

创建完临时顺序节点后,获取 "/xxx" 下的所有子节点,并对该节点注册子节点变更监听。

确定创建完的临时顺序节点在所有节点中的顺序。

对于读节点:

没有比自己序号小的节点,或比自己序号小的节点都是读节点,则成功获取到共享锁。

如果比自己序号小的节点中存在写节点,则需进入等待。

对于写节点:

如果自己不是序号最小的节点,则需进入等待。

接受到子节点变更通知后,重复步骤1

dfcc8ba83d99b14001b8e3bfce6f8583.png

以下为实现代码:

import java.io.IOException;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

import org.I0Itec.zkclient.IZkChildListener;

import org.I0Itec.zkclient.ZkClient;

import org.apache.http.client.ClientProtocolException;

/**

* 分布式共享锁

* @author alexnevsky

* @date 2018年5月23日

*/

public class SharedLock {

private static ZkClient zkClient = new ZkClient("IP1:Port1,IP2:Port2,IP3:Port3", 5000);

private static final String PARENT_PATH = "/zk-book/shared_lock";

private static volatile boolean isExecuted = false;

public static void main(String[] args) throws InterruptedException, ClientProtocolException, IOException {

String nodeTemp = zkClient.createEphemeralSequential(PARENT_PATH + "/w-", null);

String node = nodeTemp.substring(nodeTemp.lastIndexOf("/") + 1);

List currentChilds = sortNodes(zkClient.getChildren(PARENT_PATH));

if (currentChilds.size() > 0)

isExecuted = getLockAndExecute(currentChilds, node);

zkClient.subscribeChildChanges(PARENT_PATH, new IZkChildListener() {

@Override

public void handleChildChange(String parentPath, List currentChilds)

throws Exception {

if (currentChilds.size() > 0) {

currentChilds = sortNodes(currentChilds);

isExecuted = getLockAndExecute(currentChilds, node);

}

}

});

while (!isExecuted) {

Thread.sleep(Integer.MAX_VALUE);

}

}

/**

* 排序节点

* @author alexnevsky

* @date 2018年5月24日

* @param nodes

* @return

*/

private static List sortNodes(List nodes) {

Collections.sort(nodes, new Comparator() {

@Override

public int compare(String o1, String o2) {

o1 = o1.indexOf("r-") > -1 ? o1.replaceFirst("r-", "") : o1.replaceFirst("w-", "");

o2 = o2.indexOf("r-") > -1 ? o2.replaceFirst("r-", "") : o2.replaceFirst("w-", "");

return Integer.parseInt(o1) - Integer.parseInt(o2); // 比较序列号

}

});

return nodes;

}

/**

* 获取节点位置

* @author alexnevsky

* @date 2018年5月24日

* @param nodes

* @param node

* @return

*/

private static Integer getNodePosition(List nodes, String node) {

for (int i = 0, size = nodes.size(); i < size; i++) {

if (nodes.get(i).equals(node))

return i;

}

return null; // 无此数据

}

/**

* 是否得到锁

* @author alexnevsky

* @date 2018年5月24日

* @param nodes

* @param node

* @param nodePosition

* @return

*/

private static boolean isGetLock(List nodes, String node, int nodePosition) {

if (nodePosition == 0) // 没有比此序号更小的节点

return true;

if (node.indexOf("r-") > -1) { // 读节点

for (int i = 0; i < nodePosition; i++) { // 遍历小于次序号的节点

String nodeTemp = nodes.get(i);

if (nodeTemp.indexOf("w-") > -1) // 存在写节点,则进入等待锁

return false;

}

return true;

}

return false;

}

/**

* 获取锁并执行

* @author alexnevsky

* @date 2018年5月24日

* @param currentChilds

* @param node

* @return

*/

private static boolean getLockAndExecute(List currentChilds, String node) {

Integer nodePosition = getNodePosition(currentChilds, node);

if (nodePosition == null) // 子节点为空

return false;

System.out.println("子节点:" + currentChilds.toString() + ", " + node + " 的位置:" + nodePosition);

boolean isGetLock = isGetLock(currentChilds, node, nodePosition);

if (isGetLock) {

System.out.println(node + " 成功获取到锁,开始执行耗时任务");

doSomething();

boolean isSuccess = zkClient.delete(PARENT_PATH + "/" + node);

if (isSuccess)

System.out.println(node + " 成功执行完任务并删除节点");

} else {

System.out.println(node + " 未获取到锁");

}

return isGetLock;

}

private static void doSomething() {

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

测试以上代码会发现,当获取锁的节点过多时,某一节点变更会通知所有节点,会对 ZooKeeper 服务器造成巨大的性能影响和网络冲击,服务器会发送给客户端大量的事件通知。比如有以下节点,当 w-24 节点变更时,会通知给其余节点。

da873f58dba094aae5ca3a23ac27141f.png

因为当获取共享锁时,要判断比自己序号小的节点,所以应该只给 r-25 节点发送通知。针对此情况,改进后判断读写顺序为:

创建完临时顺序节点后,获取 "/xxx" 下的所有子节点。

客户端调用 getChildren() 来获取子节点列表,注意,这里不注册任何监听。

如果未获取到共享锁,那么找到比自己序号小的节点来注册监听,分为以下俩种情况:

读节点:比自己序号小的最后一个写节点注册监听

写节点:比自己序号小的最后一个节点注册监听

等待监听通知,重复步骤2

a052ef92b092bca6803442af0f551149.png

改进后的共享锁代码实现:

import java.io.IOException;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

import org.I0Itec.zkclient.IZkChildListener;

import org.I0Itec.zkclient.ZkClient;

import org.apache.http.client.ClientProtocolException;

/**

* 分布式共享锁最优

* @author alexnevsky

* @date 2018年5月23日

*/

public class SharedLockOptimal {

private static ZkClient zkClient = new ZkClient("IP1:Port1,IP2:Port2,IP3:Port3", 5000);

private static final String PARENT_PATH = "/zk-book/shared_lock";

private static String nodeFullPath = zkClient.createEphemeralSequential(PARENT_PATH + "/r-", null);

public static void main(String[] args) throws InterruptedException, ClientProtocolException, IOException {

List currentChilds = sortNodes(zkClient.getChildren(PARENT_PATH));

String node = nodeFullPath.substring(nodeFullPath.lastIndexOf("/") + 1);

boolean isReadNode = node.indexOf("r-") > -1 ? true : false, isGetLock = getLock(currentChilds, node);

System.out.println("当前所有节点:" + currentChilds.toString() + ", 该" + (isReadNode ? "读" : "写") + "节点:" + node);

if (isGetLock) {

execute(node);

System.out.println("退出程序");

System.exit(1);

} else {

String monitorNode = getMonitorNode(currentChilds, node);

System.out.println(node + " 未获取到锁,注册监听节点:" + monitorNode);

if (null != monitorNode) {

zkClient.subscribeChildChanges(PARENT_PATH + "/" + monitorNode, new IZkChildListener() {

@Override

public void handleChildChange(String parentPath, List currentChilds)

throws Exception {

main(null); // 递归调用

}

});

}

Thread.sleep(Integer.MAX_VALUE);

}

}

/**

* 排序节点

* @author alexnevsky

* @date 2018年5月24日

* @param nodes

* @return

*/

private static List sortNodes(List nodes) {

Collections.sort(nodes, new Comparator() {

@Override

public int compare(String o1, String o2) {

o1 = o1.indexOf("r-") > -1 ? o1.replaceFirst("r-", "") : o1.replaceFirst("w-", "");

o2 = o2.indexOf("r-") > -1 ? o2.replaceFirst("r-", "") : o2.replaceFirst("w-", "");

return Integer.parseInt(o1) - Integer.parseInt(o2); // 比较序列号

}

});

return nodes;

}

/**

* 获取节点位置

* @author alexnevsky

* @date 2018年5月24日

* @param currentChilds

* @param node

* @return

*/

private static Integer getNodePosition(List currentChilds, String node) {

for (int i = 0, size = currentChilds.size(); i < size; i++) {

if (currentChilds.get(i).equals(node))

return i;

}

return null;

}

/**

* 获取监听节点

* @author alexnevsky

* @date 2018年5月25日

* @param currentChilds

* @param node

* @return

*/

private static String getMonitorNode(List currentChilds, String node) {

String monitorNode = null;

Integer nodePosition = getNodePosition(currentChilds, node);

if (0 < nodePosition) { // 非首节点

if (node.indexOf("r-") > -1) { // 读节点

// 获取比当前序号小的最后一个写节点

for (int i = nodePosition - 1; i >= 0; i--) {

String tempNode = currentChilds.get(i);

if (tempNode.indexOf("w-") > -1)

return tempNode;

}

} else {

// 获取比当前序号小的最后一个节点

return currentChilds.get(nodePosition - 1);

}

}

return monitorNode;

}

/**

* 获取锁

* @author alexnevsky

* @date 2018年5月24日

* @param currentChilds

* @param node

* @return

*/

private static boolean getLock(List currentChilds, String node) {

Integer nodePosition = getNodePosition(currentChilds, node);

if (nodePosition == null)

return false;

if (nodePosition == 0) // 无序号更小的节点

return true;

if (node.indexOf("r-") > -1) { // 读节点

for (int i = 0; i < nodePosition; i++) { // 遍历前面序号的节点

String tempNode = currentChilds.get(i);

if (tempNode.indexOf("w-") > -1) // 存在写节点,返回失败

return false;

}

return true;

}

return false;

}

/**

* 执行

* @author alexnevsky

* @date 2018年5月24日

* @param node

* @return

*/

private static void execute(String node) {

System.out.println(node + " 成功获取到锁,开始执行耗时任务");

doSomething();

boolean isDeletedLock = zkClient.delete(nodeFullPath);

System.out.println(node + " 成功执行完任务,删除节点" + (isDeletedLock ? "成功" : "失败"));

}

/**

* 模拟耗时任务

* @author alexnevsky

* @date 2018年5月25日

*/

public static void doSomething() {

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值