Zookeeper的特性
- ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,可以使⽤它来进⾏分布式数据的发布与订阅。通过对ZooKeeper中丰富的数据节点类型进⾏交叉使⽤,配合Watcher事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。
- 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其⼦节点列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知
- 对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被⾃动删除
- 利⽤上述2和3,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个Watcher监听,那么但凡进⾏动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:
/clusterServers/[Hostname],
这样,监控系统就能够实时监测机器的变动情况。
服务器动态上下线监听
- 分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,⽽任意⼀台客户端都要能实时感知到主节点服务器的上下线。
思路分析
-
在pom文件中添加操作Zookeeper所需依赖
<dependencies> <!--配置日志--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <!--配置zookeeper依赖--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.6.0</version> </dependency> <!--配合测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
-
整合 log4j,在resources下创建log4j.properties
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n log4j.appender.logfile=org.apache.log4j.FileAppender log4j.appender.logfile.File=target/zookeeper.log log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
-
创建ZooKeeper客户端完成zookeeper节点操作。
package com.code.test; import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.util.List; public class TestZK { // 配置本地集群ip private String connStr = "192.168.88.128:2181,192.168.88.129:2181,192.168.88.130:2181"; /* session超时 60秒:一定不能太少,因为连接zookeeper和加载集群环境会因为性能原因延迟略高 如果时间太少,还没有创建好客户端,就开始操作节点,会报错的 */ private int sessionTimeout = 60000; // zookeeper 客户端对象 private ZooKeeper zkClient; //开始连接 @Before public void init() throws Exception { // 创建zookeeper客户端 zkClient = new ZooKeeper(connStr, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { System.out.println("监听得到反馈,进行业务处理!"); System.out.println(watchedEvent.getType()); } }); } // 创建的节点类型有: // 1. OPEN_ACL_UNSAFE:创建开放节点,允许任意操作 // 2. READ_ACL_UNSAFE:创建只读节点 // 3. CREATOR_ALL_ACL:创建者才有全部权限 @Test public void createNode() throws Exception { String nodeCreated = zkClient.create("/code2", "code".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); // 参数1:要创建的节点的路径 // 参数2:节点数据 // 参数3:节点权限 // 参数4:节点的类型 System.out.println("nodeCreated = " + nodeCreated); } // 获取节点上的数据 code2 数据 :code 修改节点上的值 Code修改后 @Test public void find() throws Exception{ byte[] bs = zkClient.getData("/code2", false, new Stat()); // 路径不存在时会报错 String data = new String(bs); System.out.println("查询到数据:"+data); } // 修改节点上的值 @Test public void update()throws Exception{ Stat stat = zkClient.setData("/code2", "Code修改后".getBytes(), 1); //先查看节点详情,获得dataVersion = 0 find(); System.out.println(stat); } // 删除节点 @Test public void delete() throws Exception { zkClient.delete("/code", 0); // 先查看节点详情,获得dataVersion = 1 System.out.println("删除成功!"); } // 获取子节点 @Test public void getChildren() throws Exception { List<String> children = zkClient.getChildren("/china",false); // false:不监听 for (String child : children) { System.out.println(child); } } //监听根节点下面的变化 @Test public void watchNode() throws Exception { List<String> children = zkClient.getChildren("/", true); // true:注册监听 for (String child : children) { System.out.println(child); } // 让线程不停止,等待监听的响应 System.in.read(); } //判断Znode是否存在 @Test public void exist() throws Exception { Stat stat = zkClient.exists("/code", false); System.out.println(stat == null ? "不存在" : "存在"); } }
模拟商家上线通知
-
商家服务类:在zookeeper集中提前创建号商家根节点信息
package com.code.meittuan; import org.apache.zookeeper.*; import java.io.IOException; public class ShopServer { // 配置本地集群ip private String connectString = "192.168.88.128:2181,192.168.88.129:2181,192.168.88.130:2181"; /* session超时 60秒:一定不能太少,因为连接zookeeper和加载集群环境会因为性能原因延迟略高 如果时间太少,还没有创建好客户端,就开始操作节点,会报错的 */ private int sessionTimeout = 60000; // zookeeper 客户端对象 private ZooKeeper zkClient; //创建客户端,连接到zookeeper public void conntect() throws Exception{ zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { public void process(WatchedEvent watchedEvent) { } }); } //注册到zookeeper public void register( String shopName )throws Exception{ //一定要创建EPHEMERAL_SEQUENTIAL 临时有序的节点(营业) // 一来可以自动编号,而来断开时,节点自动删除(打样) String s = zkClient.create("/meituan/shop", shopName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); System.out.println("【"+shopName+"】开始营业了!"+ s); } public static void main(String[] args) throws Exception{ //1.我要开一个饭店 ShopServer shop = new ShopServer(); //2. 连接zookeeper集群(和美团取得联系) shop.conntect(); //3.将服务节点注册到zookeeper(入住美团) shop.register(args[0]); //4.业务逻辑处理(做生意) shop.business(args[0]); } //做买卖 private void business(String shopName) throws Exception{ System.out.println("【"+shopName+"】正在火爆营业中!"); System.in.read(); } }
-
客户消费类
package com.code.meittuan; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.data.Stat; import org.junit.Before; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class Customers { // 配置本地集群ip private String connectString = "192.168.88.128:2181,192.168.88.129:2181,192.168.88.130:2181"; /* session超时 60秒:一定不能太少,因为连接zookeeper和加载集群环境会因为性能原因延迟略高 如果时间太少,还没有创建好客户端,就开始操作节点,会报错的 */ private int sessionTimeout = 60000; // zookeeper 客户端对象 private ZooKeeper zkClient; //创建客户端,连接到zookeeper public void connect() throws Exception { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { try { // 再次获取商家列表 getShopList(); }catch (Exception e){ e.printStackTrace(); } } }); } //获取子节点列表(获取商家列表) private void getShopList() throws Exception { //1.获取服务器的子节点信息,并且对父节点进行监听 List<String> shops = zkClient.getChildren("/meituan", true); //2.声明存储服务器信息的集合 ArrayList<String> shopList = new ArrayList<String>(); for(String shop : shops){ byte[] bytes = zkClient.getData("/meituan/" + shop, false, new Stat()); shopList.add(new String( bytes )); } System.out.println( "目前正在营业的商家:"+shopList ); } public static void main(String[] args) throws Exception { Customers client = new Customers(); // 1.获得zookeeper的连接 (用户打开美团APP) client.connect(); // 2.获取meituan下的所有子节点列表(获取商家列表) client.getShopList(); // 3.业务逻辑处理 client.business(); } private void business() throws Exception { System.out.println("用户正在浏览商家。。。"); System.in.read(); } }
-
运行客户消费类就会得到商家列表信息
目前正在营业的商家:[KFC, BurgerKing, baozi] 目前正在营业的商家:[KFC, BurgerKing, baozi] 用户正在浏览商家。。。
-
在linux中添加一个商家,然后观察客户端的控制台输出(商家列表会更新出最新商家),多添加几个,也会实时输出商家列表
-
在linux中删除商家,在客户端的控制台也会实时看到商家移除后的最新商家列表
-
运行商家服务类,以mian方法带参数的形式运行
【kfc】开始营业了!/meituan/shop0000000003
【kfc】正在火爆营业中!
分布式锁
什么是锁
- 在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全(数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串⾏执⾏消除并发修改变量。
- 对变量或者堆代码码块做同步本质上就是加锁。⽬的就是实现多个线程在⼀个时刻同⼀个代码块只能有⼀个线程可执⾏
分布式锁 :分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题,如下所示
这种设计存在线程安全问题:
假设Redis ⾥⾯的某个商品库存为 1;此时两个⽤户同时下单,其中⼀个下单请求执⾏到第 3 步,更新数据库的库存为 0,但是第 4 步还没有执⾏。⽽另外⼀个⽤户下单执⾏到了第 2 步,发现库存还是 1,就继续执⾏第 3 步。但是商品库存已经为0。所以如果数据库没有限制就会出现超卖的问题。
解决⽅法 :⽤锁把 2、3、4 步锁住,让他们执⾏完之后,另⼀个线程才能进来执⾏。
新的问题:上述方案解决的是单机,当系统应对并发不断提⾼,如果增加⼀台机器,并且还是使用此方案,会出现更⼤的问题
假设有两个下单请求同时到来,分别由两个机器执⾏,那么这两个请求是可以同时执⾏了,依然存在超卖的问题。
因为如图所示系统是运⾏在两个不同的 JVM ⾥⾯,不同的机器上,增加的锁只对⾃⼰当前 JVM ⾥⾯的线程有效,对于其他 JVM 的线程是⽆效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁是同⼀个锁,此时分布式锁就能解决该问题。
分布式锁的作⽤
- 在整个系统提供⼀个全局、唯⼀的锁,在分布式系统中每个系统在进⾏相关操作的时候需要获取到该锁,才能执⾏相应操作。
zk实现分布式锁
使⽤Zookeeper可以创建临时带序号节点的特性来实现⼀个分布式锁
- 锁就是zk指定⽬录下序号最⼩的临时序列节点,多个系统的多个线程都要在此⽬录下创建临时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利⽤节点的顺序进⾏锁的判断。
- 每个线程都是先创建临时顺序节点,然后获取当前⽬录下最⼩的节点(序号),判断最⼩节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
- 获取锁失败的线程获取当前节点上⼀个临时顺序节点,并对对此节点进⾏监听,当该节点删除的时候(上⼀个线程执⾏结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了锁
main⽅法
package com.code.dislock;
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.code.dislock;
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 {
public DisClient() {
//初始化zk的/distrilocl节点,会出现线程安全问题
synchronized (DisClient.class) {
if (!zkClient.exists("/distrilock")) {
zkClient.createPersistent("/distrilock");
}
}
}
//前⼀个节点
String beforNodePath;
String currentNoePath;
//获取到zkClient
private ZkClient zkClient = new ZkClient("linux121:2181,linux122:2181");
//把抢锁过程为量部分,⼀部分是创建节点,⽐较序号,另⼀部分是等待锁
//完整获取锁⽅法
public void getDisLock() {
//获取到当前线程名称
final String threadName = Thread.currentThread().getName();
//⾸先调⽤tryGetLock
if (tryGetLock()) {
//说明获取到锁
System.out.println(threadName + ":获取到了锁");
} else {
// 没有获取到锁,
System.out.println(threadName + ":获取锁失败,进⼊等待状态");
waitForLock();
//递归获取锁
getDisLock();
}
}
CountDownLatch countDownLatch = null;
//尝试获取锁
public boolean tryGetLock() {
//创建临时顺序节点,/distrilock/序号
if (null == currentNoePath || "".equals(currentNoePath)) {
currentNoePath = zkClient.createEphemeralSequential("/distrilock/", "lock");
}
//获取到/distrilock下所有的⼦节点
final List<String> childs = zkClient.getChildren("/distrilock");
//对节点信息进⾏排序
Collections.sort(childs); //默认是升序
final String minNode = childs.get(0);
//判断⾃⼰创建节点是否与最⼩序号⼀致
if (currentNoePath.equals("/distrilock/" + minNode)) {
//说明当前线程创建的就是序号最⼩节点
return true;
} else {
//说明最⼩节点不是⾃⼰创建,要监控⾃⼰当前节点序号前⼀个的节点
final int i = Collections.binarySearch(childs, currentNoePath.substring("/distrilock/".length()));
//前⼀个(lastNodeChild是不包括⽗节点)
String lastNodeChild = childs.get(i - 1);
beforNodePath = "/distrilock/" + lastNodeChild;
}
return false;
}
//等待之前节点释放锁,如何判断锁被释放,需要唤醒线程继续尝试tryGetLock
public void waitForLock() {
//准备⼀个监听器
final IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
//删除
@Override
public void handleDataDeleted(String s) throws Exception {
//提醒当前线程再次获取锁
countDownLatch.countDown();//把值减1变为0,唤醒之前await线程
}
};
//监控前⼀个节点
zkClient.subscribeDataChanges(beforNodePath, iZkDataListener);
//在监听的通知没来之前,该线程应该是等待状态,先判断⼀次上⼀个节点是否还存在
if (zkClient.exists(beforNodePath)) {
//开始等待,CountDownLatch:线程同步计数器
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();//阻塞,countDownLatch值变为0
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//解除监听
zkClient.unsubscribeDataChanges(beforNodePath, iZkDataListener);
}
//释放锁
public void deleteLock() {
if (zkClient != null) {
zkClient.delete(currentNoePath);
zkClient.close();
}
}
}