第六部分 Zookeeper应用实践
ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使用它来进⾏分布式数据的发布与订阅。另一⽅面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher 事件通知机制,可以⾮常⽅便地构建一系列分布式应⽤中都会涉及的核心功能,如数据发布/订阅、命名服务、集群管理理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应用场景来做下介绍
Zookeeper的两大特性:
- 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
- 对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除。
利用其两大特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个 Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。
6.1 服务器动态上下线监听
分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,⽽任意⼀台客户端都要能实时感知到主节点服务器的上下线。
思路分析
具体实现:
服务端
package com.lagou.zk;
import org.I0Itec.zkclient.ZkClient;
public class Server {
public ZkClient zkClient = null;
// 获取zk连接对象
private void connectZk(){
zkClient = new ZkClient("linux121:2181,linux122:2181,linux123:2181");
if (!zkClient.exists("/servers")){
zkClient.createPersistent("/servers");
}
}
// 服务器向ZK注册
public void registerServer(String ip, String port){
// 创建临时顺序节点
String path = zkClient.createEphemeralSequential("/servers/server", ip + ":" +port);
System.out.println("服务器注册成功。ip=" + ip + ", port=" + port + ", 节点路径:" + path);
}
public static void main(String[] args) {
Server server = new Server();
server.connectZk();
server.registerServer(args[0],args[1] );
//启动⼀个服务线程提供时间查询
new TimeServer(Integer.parseInt(args[1])).start();
}
}
服务端提供时间查询线程类
package com.lagou.zk;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class TimeServer extends Thread {
private int port;
public TimeServer(int port) {
this.port = port;
}
@Override
public void run() {
//启动serversocket监听一个端⼝
try {
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
Socket socket = serverSocket.accept();
OutputStream out = socket.getOutputStream();
out.write(new Date().toString().getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
package com.lagou.zk;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Client {
//获取zkclient
ZkClient zkClient = null;
//维护⼀个servers信息集合
ArrayList<String> infos = new ArrayList<String>();
private void connectZk() {
// 创建zkclient
zkClient = new ZkClient("linux121:2181,linux122:2181");
//第一次获取服务器信息,所有的⼦节点
List<String> childs = zkClient.getChildren("/servers");
for (String child : childs) {
//存储着ip+port
Object o = zkClient.readData("/servers/" + child);
infos.add(String.valueOf(o));
}
//对servers⽬录进行监听
zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
public void handleChildChange(String s, List<String> children) throws Exception {
//接收到通知,说明节点发⽣了变化,client需要更新infos集合中的数据
ArrayList<String> list = new ArrayList<String>();
//遍历更新过后的所有节点信息
for (String path : children) {
Object o = zkClient.readData("/servers/" + path);
list.add(String.valueOf(o));
}
//最新数据覆盖老数据
infos = list;
System.out.println("接收到通知,最新服务器信息为:" + infos);
}
});
}
//发送时间查询的请求
public void sendRequest() throws IOException {
//⽬标服务器地址
Random random = new Random();
int i = random.nextInt(infos.size());
String ipPort = infos.get(i);
String[] arr = ipPort.split(":");
//建立socket连接
Socket socket = new Socket(arr[0], Integer.parseInt(arr[1]));
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
//发送数据
out.write("query time".getBytes());
out.flush();
//接收返回结果
byte[] b = new byte[1024];
in.read(b); //读取服务端返回数据
System.out.println("client端接收到server:+" + ipPort + "+返回结果:" + new String(b));
//释放资源
in.close();
out.close();
socket.close();
}
public static void main(String[] args) throws InterruptedException {
Client client = new Client();
client.connectZk(); //监听器逻辑
while (true) {
try {
client.sendRequest(); //发送请求
} catch (IOException e) {
e.printStackTrace();
try {
client.sendRequest();
} catch (IOException e1) {
e1.printStackTrace();
}
}
//每隔⼏秒发送⼀次请求到服务端
Thread.sleep(2000);
}
}
}
6.2 分布式锁
1. 什么是锁
在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全 (数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串行执⾏消除并发修改变量。 对变量或者堆代码块做同步本质上就是加锁。目的就是实现多个线程在一个时刻同一个代码块只能有⼀个线程可执⾏
2. 分布式锁
分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题。观察下面的例子
上⾯的设计是存在线程安全问题
问题
假设Redis ⾥面的某个商品库存为1;此时两个用户同时下单,其中⼀个下单请求执⾏到第 3 步,更新数据库的库存为0,但是第 4 步还没有执行。
⽽另外⼀个用户下单执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。但是商品库存已经为0, 所以如果数据库没有限制就会出现超卖的问题。
解决方法:⽤锁把 2、3、4 步锁住,让他们执行完之后,另一个线程才能进来执行。
公司业务发展迅速,系统应对并发不断提高,解决方案是要增加⼀台机器,结果会出现更大的问题
假设有两个下单请求同时到来,分别由两个机器执行,那么这两个请求是可以同时执⾏了,依然存在超卖的问题。
因为如图所示系统是运行在两个不同的 JVM ⾥面,不同的机器上,增加的锁只对⾃己当前 JVM ⾥面的线程有效,对于其他 JVM 的线程是⽆效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁是同⼀个锁,此时分布式锁就能解决该问题。
分布式锁的作用:在整个系统提供一个全局、唯⼀的锁,在分布式系统中每个系统在进行相关操作的时候需要获取到该锁,才能执行相应操作。
3.zk实现分布式锁
利用Zookeeper可以创建临时带序号节点的特性来实现⼀个分布式锁
实现思路
锁就是zk指定目录下序号最⼩的临时顺序节点,多个系统的多个线程都要在此目录下创建临时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
每个线程都是先创建临时顺序节点,然后获取当前⽬录下最小的节点(序号),判断最⼩节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
获取锁失败的线程获取当前节点上一个临时顺序节点,并对此节点进行监听,当该节点删除的时候(上⼀个线程执行结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了锁。
流程图
Java代码实现分布式锁
main方法
package com.lagou.zk.lock;
/**
* 使用多线程模拟分布式集群,实现zookeeper分布式锁的使用
*/
public class DisLockTest {
public static void main(String[] args) {
//使用10个线程模拟分布式环境
for (int i = 0; i < 10; i++) {
new Thread(new DisLockRunnable()).start();//启动线程
}
}
static class DisLockRunnable implements Runnable {
public void run() {
//每个线程具体的任务,每个线程就是抢锁,
final DisClient client = new DisClient();
client.getDisLock();
try {
//模拟获取锁之后的其它动作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放锁
client.deleteLock();
}
}
}
客户端,核心实现
package com.lagou.zk.lock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 客户端抢锁:
* 1.客户端在zk创建临时顺序节点,并获取到序号
* 2.判断自己创建的节点序号是否是最小节点序号,如果是则获取锁,执行相关操作,最后释放锁
* 3.如果不是最小节点,当前线程需要等待,并监听你前一个序号节点被删除(释放锁),然后再次判断自己是否是最小节点。
*/
public class DisClient {
// 获取zk对象
private ZkClient zkClient = new ZkClient("linux121:2181,linux122:2181");
private String basePath = "/distriLock";
public DisClient() {
synchronized (DisClient.class){
if (!zkClient.exists(basePath)) {
zkClient.createPersistent(basePath);
}
}
}
// 获取分布式锁
public void getDisLock() {
String threadName = Thread.currentThread().getName();
if (tryGetLock()) {
System.out.println(threadName + "获取到了锁.");
} else {
System.out.println(threadName + "获取锁失败,进入等待状态.");
waitForLock();
// 递归获取锁
getDisLock();
}
}
String currentNodePath = null;
String beforeNodePath = null;
// 尝试获取锁
private boolean tryGetLock() {
// 创建临时顺序节点:/distriLock/序号
if (currentNodePath == null || "".equals(currentNodePath)) {
currentNodePath = zkClient.createEphemeralSequential(basePath + "/", "lock");
}
// 获取basePath下所有的子节点
List<String> children = zkClient.getChildren(basePath);
// 对所有子节点排序(默认升序)
Collections.sort(children);
// 获取第一个节点即为最小节点
String minNode = children.get(0);
// 判断自己是否与最小节点一致
if (currentNodePath.equals(basePath + "/" + minNode)) {
return true;
} else {
// 自己不是最小节点,需要监听自己前一个节点,获取前一个节点
int i = Collections.binarySearch(children, currentNodePath.substring("/distriLock/".length()));
String preNode = children.get(i - 1);
beforeNodePath = basePath + "/" + preNode;
return false;
}
}
CountDownLatch countDownLatch = null;
// 等待前一个节点释放锁
private void waitForLock() {
// 创建前一个节点的监听器
IZkDataListener iZkDataListener = new IZkDataListener() {
public void handleDataChange(String s, Object o) throws Exception {
}
public void handleDataDeleted(String s) throws Exception {
// 提醒当前线程再次获取锁
countDownLatch.countDown();//把值减1变为0,唤醒之前await线程
}
};
// 监听前一个节点
zkClient.subscribeDataChanges(beforeNodePath, iZkDataListener);
//在监听的通知没来之前,该线程应该是等待状态,先判断一次上一个节点是否还存在
if (zkClient.exists(beforeNodePath)) {
//开始等待,CountDownLatch:线程同步计数器
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();//阻塞,countDownLatch值变为0
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 解除监听
zkClient.unsubscribeDataChanges(beforeNodePath, iZkDataListener);
}
// 释放锁
public void deleteLock() {
if (zkClient != null) {
zkClient.delete(currentNodePath);
zkClient.close();
}
}
}
分布式锁的实现可以是 Redis、Zookeeper,相对来说生产环境如果使用分布式锁可以考虑使用Redis实现⽽非Zk。