目录
1.配置中心
工作中是否有这样的一个场景:数据库用户和密码信息放在一个配置文件中,应用读取该配置文件,配置文件信息放入缓存。若数据库的用户名和密码改变,还需要重新加载缓存,比较麻烦,通过zookeeper可以轻松完成,当数据库发生变化时自动完成缓存同步。
设计思路:
- 连接zookeeper服务器
- 读取zookeeper中的配置信息,注册watcher监听器,存入本地变量
- 当zookeeper中的配置信息发生变化时,通过watcher的回调方法捕获数据变化事件
- 重新获取配置信息
package com.cjian.zookeeper;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: CJ
* @time: 2021/1/29 15:27
*/
public class MyConfigCenter implements Watcher {
// zk连接对象
ZooKeeper zooKeeper;
// 计数器对象
private static CountDownLatch countDownLatch = new CountDownLatch(1);
// zk的连接串
private String ip = "127.0.0.1:2181";
// 用于本地化配置存储信息
private String url;
private String username;
private String password;
// 构造方法
public MyConfigCenter() {
initValue();
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public void process(WatchedEvent event) {
try {
// 时间类型
if (event.getType() == Watcher.Event.EventType.None) {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("连接创建成功");
countDownLatch.countDown();
} else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
System.out.println("断开连接");
} else if (event.getState() == Watcher.Event.KeeperState.Expired) {
System.out.println("会话超时");
zooKeeper = new ZooKeeper(ip, 5000, this); // 重连
} else if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
System.out.println("验证失败");
}
} else if (event.getType() == Event.EventType.NodeDataChanged) { // 当配置信息发生变化,就重新去加载配置信息
initValue();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接zookeeper服务器,读取配置信息
*/
private void initValue() {
//创建连对象
try {
// 创建链接
if (zooKeeper == null) {
zooKeeper = new ZooKeeper(ip, 5000, this);
}
// 阻塞线程,等待连接成功
countDownLatch.await();
// 读取配置信息
this.url = new String(zooKeeper.getData("/config/url", true, null));
this.username = new String(zooKeeper.getData("/config/username", true, null));
this.password = new String(zooKeeper.getData("/config/password", true, null));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试
*/
public static void main(String[] args) {
try {
MyConfigCenter myConfigCenter = new MyConfigCenter();
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(5);
System.out.println(myConfigCenter);
System.out.println("==================================================");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "MyConfigCenter{" + "url='" + url + '\'' + ", username='" + username + '\'' + ", password='" + password
+ '\'' + '}';
}
}
zookeeper客户端先创建好相关节点:
[zk: localhost:2181(CONNECTED) 104] create /config "config"
Created /config
[zk: localhost:2181(CONNECTED) 105] create /config/url "127.0.0.1:3306"
Created /config/url
[zk: localhost:2181(CONNECTED) 107] create /config/username "cjian"
Created /config/username
[zk: localhost:2181(CONNECTED) 108] create /config/password "111111"
Created /config/password
运行main方法后,执行: set /config/password "123456"
控制台输出:
连接创建成功
MyConfigCenter{url='127.0.0.1:3306', username='cjian', password='111111'}
==================================================
MyConfigCenter{url='127.0.0.1:3306', username='cjian', password='111111'}
==================================================
MyConfigCenter{url='127.0.0.1:3306', username='cjian', password='111111'}
==================================================
MyConfigCenter{url='127.0.0.1:3306', username='cjian', password='123456'}
==================================================
MyConfigCenter{url='127.0.0.1:3306', username='cjian', password='123456'}
==================================================
2.生成分布式唯一ID
在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID。但是分库分表后,已经无法满足了,此时我们就可以用zookeeper在分布式环境下生成全局唯一ID.
设计思路:
- 连接Zookeeper;
- 指定路径生成临时有序节点;
- 取序列号即为分布式环境下的唯一ID.
package com.cjian.zookeeper;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
/**
* @description:
* @author: CJ
* @time: 2021/2/1 11:46
*/
public class GloballyUniqueId implements Watcher{
ZooKeeper zooKeeper;
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private String ip = "127.0.0.1:2181";
// 用户生成序号的节点
String defaultPath = "/Id";
public GloballyUniqueId() {
try {
zooKeeper = new ZooKeeper(ip, 5000, this);
// 阻塞线程
countDownLatch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void process(WatchedEvent event) {
try {
// 时间类型
if (event.getType() == Watcher.Event.EventType.None) {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("连接创建成功");
countDownLatch.countDown();
} else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
System.out.println("断开连接");
} else if (event.getState() == Watcher.Event.KeeperState.Expired) {
System.out.println("会话超时");
zooKeeper = new ZooKeeper(ip, 5000, this); // 重连
} else if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
System.out.println("验证失败");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成ID的方法
*/
public String getUnqiueId() {
String path = "";
try {
//创建临时的有序节点
path = zooKeeper.create(defaultPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (Exception e) {
e.printStackTrace();
}
return path.substring(3);
}
/**
* 测试
*/
public static void main(String[] args) {
GloballyUniqueId globallyUniqueId = new GloballyUniqueId();
for (int i = 0; i < 5; i++) {
String unqiueId = globallyUniqueId.getUnqiueId();
System.out.println(unqiueId);
}
}
}
测试:
连接创建成功
0000000047
0000000048
0000000049
0000000050
0000000051
3.分布式锁
分布式锁有多种实现方式,比如通过数据库、redis都可以实现。作为分布式协同工具Zookeeper当然也有着标准的实现方式,下面介绍在zookeeper中如何实现排他锁。
设计思路:
- 每个客户端往/Locks下创建临时有序节点/Locks/Lock_,创建成功后/Locks下面会有每个客户端对应的节点,如//Locks/Lock_000000001;
- 客户端取得/Locks下子节点,并进行排序,判断排在最前的是否为自己,如果自己的锁节点排在第一位,代表获得锁成功;
- 如果自己的锁节点不在第一位,则监听自己前一位的锁节点,例如,自己锁节点Lock_000000002,那么监听/Lock_000000001
- 当前一位锁节点/Lock_000000001 对应的客户端执行完毕,释放了锁,将会出触发监听客户端(Lock_000000002)的逻辑
- 监听客户端重新执行第二步的逻辑,判断自己是否获得了锁。
package com.cjian.zookeeper;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* @description:
* @author: CJ
* @time: 2021/2/1 14:03
*/
public class MyLock {
// zk连接对象
public static ZooKeeper zooKeeper;
// 计数器对象
private static CountDownLatch countDownLatch = new CountDownLatch(1);
// zk的连接串
private static final String IP = "127.0.0.1:2181";
// 锁的节点名称
public static final String LOCK_ROOT_PATH = "/Locks";
public static final String LOCK_NODE_NAME = LOCK_ROOT_PATH + "/lock_";
private String lockPath;
public MyLock() {
try {
zooKeeper = new ZooKeeper(IP, 5000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType() == Event.EventType.None){
if (watchedEvent.getState() ==Event.KeeperState.SyncConnected ){
System.out.println("连接成功!");
countDownLatch.countDown();
}
}
}
});
countDownLatch.await();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 获取锁
*/
public void acquireLock() {
//创建节点
createLock();
//尝试获取锁
attemptLock();
}
/**
* 创建锁节点
*/
private void createLock() {
try {
Stat exists = zooKeeper.exists(LOCK_ROOT_PATH, false);
if(exists == null){
zooKeeper.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//创建临时有序节点
lockPath = zooKeeper.create(LOCK_NODE_NAME, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(lockPath+ " 创建成功");
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if(watchedEvent.getType() == Event.EventType.NodeDeleted){
//有节点被删除,则唤醒所有等待的线程
synchronized (this){
notifyAll();
}
}
}
};
/**
* 尝试获取锁
*/
private void attemptLock() {
try {
//思路:
//1.获取/Locks下的所有节点,只包含子节点的名称
List<String> children = zooKeeper.getChildren(LOCK_ROOT_PATH, false);
//2.对所有子节点排序
Collections.sort(children);
//3.判断当前节点在集合的位置
int index = children.indexOf(lockPath.substring((LOCK_NODE_NAME.length() - LOCK_ROOT_PATH.length())+1));
if(index == 0){
System.out.println("获取锁成功!");
}else{
//上一个节点的位置
String prevPath= children.get(index -1);
Stat exists = zooKeeper.exists(LOCK_ROOT_PATH + "/" + prevPath, watcher);
if(exists == null){//说明上一个节点被删除了->锁释放
attemptLock();
}else{
//当前线程等待
synchronized (watcher){
watcher.wait();
}
attemptLock();
}
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 释放锁
*/
private void releaseLock() {
try {
zooKeeper.delete(lockPath, -1);
zooKeeper.close();
System.out.println(lockPath+" 锁已释放!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public static void sall(){
System.out.println("售票开始");
// 线程休眠,模拟现实中耗时的操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("售票结束");
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
MyLock myLock = new MyLock();
// 获取锁
myLock.acquireLock();
sall();
// 释放锁
myLock.releaseLock();
}
}
}
开启两个main方法:
运行结果如下: