Zookeeper系列之高级篇
1. NIO、ZAB协议、2PC提交相关概念
2. Leader选举
3. 手写分布式锁,配置中心
====================================
一. NIO、ZAB协议、2PC提交相关概念
1 NIO
NIO中的几个概念
Channel
可以将NIO中的Channel同传统IO中的Stream来类比,但是要注意,传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutStream只能写操作。而Channel是双向的,即可用来进行读操作,又可用来进行写操作
Buffer
在NIO中所有数据的读和写都离不开Buffer,读取的数据只能放在Buffer中,写入的数据也是先写入到Buffer中
Seletor
将Channel和Seletor配合使用,必须将channel注册到Seletor上,通过SelectableChannel.register()方法来实现
2 2PC
(Two Phase Commitment Protocol)当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID 特性,就需要引入一个“协调者”(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为AP。TM负责调度AP的行为,并最终决定这些AP是否要把事务真正进行提交;因为整个事务是分为两个阶段提交,所以叫2pc
阶段一:提交事务请求(投票)
- 事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应 - 执行事务
各个参与者节点执行事务操作,并将Undo和Redo信息记录到事务日志中,尽量把提交过程中所有消耗时间的操作和准备都提前完成确保后面100%成功提交事务 - 各个参与者向协调者反馈事务询问的响应
如果各个参与者成功执行了事务操作,那么就反馈给参与者yes的响应,表示事务可以执行;如果参与者没有成功执行事务,就反馈给协调者no的响应,表示事务不可以执行,上面这个阶段有点类似协调者组织各个参与者对一次事务操作的投票表态过程,因此2pc协议的第一个阶段称为“投票阶段”,即各参与者投票表名是否需要继续执行接下去的事务提交操作。
阶段二:执行事务提交
在这个阶段,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下包含两种可能:执行事务、中断事务
3 ZAB协议
ZAB(ZookeeperAtomicBroadcast)协议是为分布式协调服务ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。在ZooKeeper中,主要依赖ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
zab协议介绍
ZAB协议包含两种基本模式,分别是
- 崩溃恢复
- 原子广播
当整个集群在启动时,或者当leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的Leader,当leader服务器选举出来后,并且集群中有过半的机器和该leader节点完成数据同步后(同步指的是数据同步,用来保证集群中过半的机器能够和leader服务器的数据状态保持一致),ZAB协议就会退出恢复模式。当集群中已经有过半的Follower节点完成了和Leader状态同步以后,那么整个集群就进入了消息广播模式。这个时候,在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和leader节点进行数据同步。同步完成后即可正常对外提供非事务请求的处理。
消息广播的实现原理
如果大家了解分布式事务的2pc和3pc协议的话(不了解也没关系,我们后面会讲),消息广播的过程实际上是一个简化版本的二阶段提交过程
- eader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较既可以实现因果有序这个特征
- leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的follower
- 当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack
- 当leader接收到合法数量(超过半数节点)的ACK后,leader就会向这些follower发送commit命令,同时会在本地执行该消息
- 当follower收到消息的commit命令以后,会提交该消息
leader的投票过程,不需要Observer的ack,也就是Observer不需要参与投票过程,但是Observer必须要同步Leader的数据从而在处理请求的时候保证数据的一致性
崩溃恢复(数据恢复)
ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下是没有任何问题的,但是一旦Leader节点崩溃,或者由于网络问题导致Leader服务器失去了过半的Follower节点的联系(leader失去与过半follower节点联系,可能是leader节点和follower节点之间产生了网络分区,那么此时的leader不再是合法的leader了),那么就会进入到崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader为了使leader挂了后系统能正常工作,需要解决以下两个问题:
- 已经被处理的消息不能丢失
当leader 收到合法数量follower 的ACKs 后,就向各个follower 广播COMMIT 命令,同时也会在本地执行COMMIT 并向连接的客户端返回「成功」。但是如果在各个follower 在收到COMMIT 命令前leader 就挂了,导致剩下的服务器并没有执行都这条消息。
leader对事务消息发起commit操作,但是该消息在follower1上执行了,但是follower2还没有收到commit,就已经挂了,而实际上客户端已经收到该事务消息处理成功的回执了。所以在zab协议下需要保证所有机器都要执行这个事务消息 - 被丢弃的消息不能再次出现
当leader 接收到消息请求生成proposal 后就挂了,其他follower 并没有收到此proposal,因此经过恢复模式重新选了leader 后,这条消息是被跳过的。此时,之前挂了的leader 重新启动并注册成了follower,他保留了被跳过消息的proposal 状态,与整个系统的状态是不一致的,需要将其删除。ZAB协议需要满足上面两种情况,就必须要设计一个leader选举算法:能够确保已经被leader提交的事务Proposal能够提交、同时丢弃已经被跳过的事务Proposal。针对这个要求
- 如果leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有已经提交的提案。因为所有提案被COMMIT 之前必须有超过半数的followerACK,即必须有超过半数节点的服务器的事务日志上有该提案的proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被COMMIT 消息的proposal 状态另外一个,zxid是64位,高32位是epoch编号,每经过一次Leader选举产生一个新的leader,新的leader会将epoch号+1,低32位是消息计数器,每接收到一条消息这个值+1,新leader选举后这个值重置为0.这样设计的好处在于老的leader挂了以后重启,它不会被选举为leader,因此此时它的zxid肯定小于当前新的leader。当老的leader作为follower接入新的leader后,新的leader会让它将所有的拥有旧的epoch 号的未被COMMIT 的proposal 清除
4 ZXID
zxid,也就是事务id,为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch(ZAB协议通过epoch编号来区分Leader周期变化的策略)用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期。低32位用于递增计数。
epoch:可以理解为当前集群所处的年代或者周期,每个leader 就像皇帝,都有自己的年号,所以每次改朝换代,leader 变更之后,都会在前一个年代的基础上加1。这样就算旧的leader 崩溃恢复之后,也没有人听他的了,因为follower 只听从当前年代的leader 的命令。*
二. Leader选举
Leader选举会分两个过程启动的时候的leader选举、leader崩溃的时候的的选举
服务器启动时的leader选举
每个节点启动的时候状态都是LOOKING,处于观望状态,接下来就开始进行选主流程进行Leader选举,至少需要两台机器(具体原因前面已经讲过了),我们选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,它本身是无法进行和完成Leader选举,当第二台服务器Server2启动时,这个时候两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下
- 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID、epoch,使用(myid,ZXID,epoch)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
- 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。
- 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
- i.优先检查ZXID。ZXID比较大的服务器优先作为
- ii.如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2,0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2,0),然后重新投票,对于Server2而言,它不需要更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
- 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2,0)的投票信息,此时便认为已经选出了Leader。
- 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。
运行过程中的leader选举
当集群中的leader服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的Leader选举,服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。
- 变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
- 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票。与启动时过程相同。
- 处理投票。与启动时过程相同,此时,Server1将会成为Leader。
- 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同
ZookeeperMain-客户端启动类
- ZookeeperMain接收客户端命令
- ZookeeperMain将客户端命令转化为Request
Zookeeper
Zookeeper调用ClientCnxn.submitRequest方法将Request包装成Packet并添加到outgoingQueue队列中
ClientCnxn
sendThread
- 连接到服务器并且进行重试
- 发送ping
- doIO
发送数据 - 从outgoingQueue中取出数据并发送给服务端
- 将需要等待结果的Packet加入到pendingQueue中
读取数据 - 接收watcher事件通知,并且把通知加入到waitingEvents队列中去
- 将pengdingQueue中的Packet读取出来并且使用服务端返回的结果进行装配
- 如果是同步请求则唤醒线程
- 如果是异步请求则将Packet加入到waitingEvents队列中
EventThread
- 从waitingEvents队列中取出数据
- 如果是watcher事件通知,出发绑定的watcher逻辑
- 如果是异步请求,则调用对应的异步回调函数
QuorumPeerMain-服务端启动类
- 解析配置
- 根据配置进行单机或集群模式的启动(判断条件就是配置文件中的servers的数量)
ZookeeperServerMain-单机模式启动类
- 初始化ZookeeperServer
- 初始化FileTxnSnapLog
- 初始化NIOServerCnxnFactory
- 启动NIOServerCnxnFactory
- 启动ZookeeperServer
ServerConfig
单机模式下的配置类,配置属性比集群模式下的少一点
ZookeeperServer-zk服务器
- 初始化ZKDatabase
- 初始化DataTree
- 从ShapShot中还原DataTree
- 开启Session检查器
- 设置请求处理器RequestProccessor
FileTxnSnapLog
事务日志和快照持久化工具类
- TxnLog-事务日志
- SnapShot-快照日志
ServerCnxnFactory
服务器上下文工厂类,负责创建服务器上下文,默认为NIOServerCnxnFactory
- 开启ServerSocketChannel
NIOServerCnxnFactory
- 启动ZookeeperThread,这个类开启的其实就是NIOServerCnxnFactory自己
- 接受到客户端的连接事件,就初始化出来一个NIOServerCnxn
- 接收数据或写出数据,NIOServerCnxn.doIO
读数据:
- 读取的是ConnectRequest,服务端新建一个session,并生成一个新的sessionId,并生成一个Request调用submitRequest方法
- 读取的正常操作请求,也会生成一个Request调用submitRequest方法
提交请求的submitRequest方法
RequestProcessor
单机模式下:
- firstProcessor=PrepRequestProcessor(线程)
- PrepRequestProcessor.next=SyncRequestProcessor(线程)
- SyncRequestProcessor=FinalRequestProcessor(线程)
PrepRequestProcessor-接收到客户端请求生成txn事务以及节点修改记录
- processRequest方法将Request添加到submittedRequests队列中
- 线程不停的从submittedRequest获取请求
- 根据请求的类型进入到不同的处理,我们以create为例
- 获取父节点信息
- 校验ACL
- 临时节点与顺序节点逻辑
- 生成txn事务
- 生成父节点修改记录
- 生成新增节点修改记录
- 将修改记录加入到outstandingChanges队列中
- 调用nextProcessor.processRequest(request);
SyncRequestProcessor
- processRequest方法将Request添加到queueRequest队列中
- 负责从queuedRequests队列中获取Request
- 负责将txn同步到磁盘,并且进行快照
- 如果同步完成了就会调用nextProcessor.processRequest(si);
FinalRequestProcessor
- 从outstandingChanges队列中获取Request
- 更新DataTree
- 触发watcher
- 构造Response
- 通过NIOServerCnxn中的sendResponse转化成ByteBuffer返回客户端
三. 手写分布式锁,配置中心
基于Zookeeper的分布式锁
public interface DistributedLock {
/*
* 获取锁,如果没有得到就等待
*/
public void acquire() throws Exception;
/*
* 获取锁,直到超时
*/
public boolean acquire(long time, TimeUnit unit) throws Exception;
/*
* 释放锁
*/
public void release() throws Exception;
}
public class SimpleDistributedLockMutex extends BaseDistributedLock implements
DistributedLock {
//锁名称前缀,成功创建的顺序节点如lock-0000000000,lock-0000000001,...
private static final String LOCK_NAME = "lock-";
// zookeeper中locker节点的路径
private final String basePath;
// 获取锁以后自己创建的那个顺序节点的路径
private String ourLockPath;
private boolean internalLock(long time, TimeUnit unit) throws Exception {
ourLockPath = attemptLock(time, unit);
return ourLockPath != null;
}
public SimpleDistributedLockMutex(ZkClientExt client, String basePath){
super(client,basePath,LOCK_NAME);
this.basePath = basePath;
}
// 获取锁
public void acquire() throws Exception {
if ( !internalLock(-1, null) ) {
throw new IOException("连接丢失!在路径:'"+basePath+"'下不能获取锁!");
}
}
// 获取锁,可以超时
public boolean acquire(long time, TimeUnit unit) throws Exception {
return internalLock(time, unit);
}
// 释放锁
public void release() throws Exception {
releaseLock(ourLockPath);
}
}
public class BaseDistributedLock {
private final ZkClientExt client;
private final String path;
private final String basePath;
private final String lockName;
private static final Integer MAX_RETRY_COUNT = 10;
public BaseDistributedLock(ZkClientExt client, String path, String lockName){
this.client = client;
this.basePath = path;
this.path = path.concat("/").concat(lockName);
this.lockName = lockName;
}
// 删除成功获取锁之后所创建的那个顺序节点
private void deleteOurPath(String ourPath) throws Exception{
client.delete(ourPath);
}
// 创建临时顺序节点
private String createLockNode(ZkClient client, String path) throws Exception{
return client.createEphemeralSequential(path, null);
}
// 等待比自己次小的顺序节点的删除
private boolean waitToLock(long startMillis, Long millisToWait, String ourPath) throws Exception{
boolean haveTheLock = false;
boolean doDelete = false;
try {
while ( !haveTheLock ) {
// 获取/locker下的经过排序的子节点列表
List<String> children = getSortedChildren();
// 获取刚才自己创建的那个顺序节点名
String sequenceNodeName = ourPath.substring(basePath.length()+1);
// 判断自己排第几个
int ourIndex = children.indexOf(sequenceNodeName);
if (ourIndex < 0){ // 网络抖动,获取到的子节点列表里可能已经没有自己了
throw new ZkNoNodeException("节点没有找到: " + sequenceNodeName);
}
// 如果是第一个,代表自己已经获得了锁
boolean isGetTheLock = ourIndex == 0;
// 如果自己没有获得锁,则要watch比我们次小的那个节点
String pathToWatch = isGetTheLock ? null : children.get(ourIndex - 1);
if ( isGetTheLock ){
haveTheLock = true;
} else {
// 订阅比自己次小顺序节点的删除事件
String previousSequencePath = basePath .concat( "/" ) .concat( pathToWatch );
final CountDownLatch latch = new CountDownLatch(1);
final IZkDataListener previousListener = new IZkDataListener() {
public void handleDataDeleted(String dataPath) throws Exception {
latch.countDown(); // 删除后结束latch上的await
}
public void handleDataChange(String dataPath, Object data) throws Exception {
// ignore
}
};
try {
//订阅次小顺序节点的删除事件,如果节点不存在会出现异常
client.subscribeDataChanges(previousSequencePath, previousListener);
if ( millisToWait != null ) {
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 ) {
doDelete = true; // timed out - delete our node
break;
}
latch.await(millisToWait, TimeUnit.MICROSECONDS); // 在latch上await
} else {
latch.await(); // 在latch上await
}
// 结束latch上的等待后,继续while重新来过判断自己是否第一个顺序节点
}
catch ( ZkNoNodeException e ) {
//ignore
} finally {
client.unsubscribeDataChanges(previousSequencePath, previousListener);
}
}
}
}
catch ( Exception e ) {
//发生异常需要删除节点
doDelete = true;
throw e;
} finally {
//如果需要删除节点
if ( doDelete ) {
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
private String getLockNodeNumber(String str, String lockName) {
int index = str.lastIndexOf(lockName);
if ( index >= 0 ) {
index += lockName.length();
return index <= str.length() ? str.substring(index) : "";
}
return str;
}
// 获取/locker下的经过排序的子节点列表
List<String> getSortedChildren() throws Exception {
try{
List<String> children = client.getChildren(basePath);
Collections.sort(
children, new Comparator<String>() {
public int compare(String lhs, String rhs) {
return getLockNodeNumber(lhs, lockName).compareTo(getLockNodeNumber(rhs, lockName));
}
}
);
return children;
} catch (ZkNoNodeException e){
client.createPersistent(basePath, true);
return getSortedChildren();
}
}
protected void releaseLock(String lockPath) throws Exception{
deleteOurPath(lockPath);
}
protected String attemptLock(long time, TimeUnit unit) throws Exception {
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
int retryCount = 0;
//网络闪断需要重试一试
while ( !isDone ) {
isDone = true;
try {
// 在/locker下创建临时的顺序节点
ourPath = createLockNode(client, path);
// 判断自己是否获得了锁,如果没有获得那么等待直到获得锁或者超时
hasTheLock = waitToLock(startMillis, millisToWait, ourPath);
} catch ( ZkNoNodeException e ) { // 捕获这个异常
if ( retryCount++ < MAX_RETRY_COUNT ) { // 重试指定次数
isDone = false;
} else {
throw e;
}
}
}
if ( hasTheLock ) {
return ourPath;
}
return null;
}
}
public class TestDistributedLock {
public static void main(String[] args) {
final ZkClientExt zkClientExt1 = new ZkClientExt("192.168.1.105:2181", 5000, 5000, new BytesPushThroughSerializer());
final SimpleDistributedLockMutex mutex1 = new SimpleDistributedLockMutex(zkClientExt1, "/Mutex");
final ZkClientExt zkClientExt2 = new ZkClientExt("192.168.1.105:2181", 5000, 5000, new BytesPushThroughSerializer());
final SimpleDistributedLockMutex mutex2 = new SimpleDistributedLockMutex(zkClientExt2, "/Mutex");
try {
mutex1.acquire();
System.out.println("Client1 locked");
Thread client2Thd = new Thread(new Runnable() {
public void run() {
try {
mutex2.acquire();
System.out.println("Client2 locked");
mutex2.release();
System.out.println("Client2 released lock");
} catch (Exception e) {
e.printStackTrace();
}
}
});
client2Thd.start();
Thread.sleep(5000);
mutex1.release();
System.out.println("Client1 released lock");
client2Thd.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ZkClientExt extends ZkClient {
public ZkClientExt(String zkServers, int sessionTimeout, int connectionTimeout, ZkSerializer zkSerializer) {
super(zkServers, sessionTimeout, connectionTimeout, zkSerializer);
}
@Override
public void watchForData(final String path) {
retryUntilConnected(new Callable<Object>() {
public Object call() throws Exception {
Stat stat = new Stat();
_connection.readData(path, stat, true);
return null;
}
});
}
}
基于Zookeeper的分布式配置中心
public class Config {
private final static String CONNECTSTRING = "39.105.157.149:2181," +
"39.105.157.149:2182," +
"39.105.157.149:2183," +
"39.105.157.149:2184";
private static final String CONFIG_PREFIX = "/config";
public static CuratorFramework curatorFramework = null;
//充当缓存
private Map<String, String> cache = new HashMap<>();
//初始化zookeeper连接
public static synchronized CuratorFramework getInstance() {
if (curatorFramework == null) {
curatorFramework = CuratorFrameworkFactory.builder()
.connectString(CONNECTSTRING)
.sessionTimeoutMs(1000)
.connectionTimeoutMs(1000)
.retryPolicy(new ExponentialBackoffRetry(1000, 1000))
.build();
curatorFramework.start();
}
return curatorFramework;
}
public Config() {
this.curatorFramework = Config.getInstance();
this.init();
}
//监听配置
public void init() {
try {
//获取连接
curatorFramework = Config.getInstance();
//获取CONFIG_PREFIX的子节点 并将数据保存到缓存中
curatorFramework.getChildren().forPath(CONFIG_PREFIX).stream().forEach(k -> {
try {
String value = new String(curatorFramework.getData().forPath(CONFIG_PREFIX + "/" + k));
cache.put(k, value);
} catch (Exception e) {
e.printStackTrace();
}
});
//绑定一个监听 cacheData设为true 当事件发生后可以拿到节点发送的内容
//使该配置文件的每个应用机器都需要坚挺 这里只用于演示
PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorFramework, CONFIG_PREFIX, true);
//添加监听
pathChildrenCache.getListenable().addListener((curatorFramework1, pathChildrenCacheEvent) -> {
String path = pathChildrenCacheEvent.getData().getPath();
//判断节点是否以制定开头开始
if (path.startsWith(CONFIG_PREFIX)) {
String key = path.replace(CONFIG_PREFIX + "/", "");
switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED:
case CHILD_UPDATED: //添加节点或者修改节点数据时 修改缓存数据
cache.put(key, new String(pathChildrenCacheEvent.getData().getData()));
break;
case CHILD_REMOVED: //节点删除时 从缓存中删除数据
cache.remove(key);
break;
}
if (PathChildrenCacheEvent.Type.CHILD_ADDED.equals(pathChildrenCacheEvent.getType()) ||
PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(pathChildrenCacheEvent.getType())) {
cache.put(key, new String(pathChildrenCacheEvent.getData().getData()));
}
// 子节点被删除时 从缓存中删除
if (PathChildrenCacheEvent.Type.CHILD_REMOVED.equals(pathChildrenCacheEvent.getType())) {
cache.remove(key);
}
}
});
//启动监听
pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);//POST_INITIALIZED_EVENT模式 会刷新缓存
} catch (Exception e) {
}
}
//保存配置信息
public void save(String name, String value) throws Exception {
curatorFramework = Config.getInstance();
String path = CONFIG_PREFIX + "/" + name;
//判断节点是否存在 如果不存在就创建
Stat stat = curatorFramework.checkExists().forPath(path);
if (stat == null) //如果不存在该节点就创建
curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);
curatorFramework.setData().forPath(path, value.getBytes());
//缓存中存入数据
cache.put(name, value);
}
//获取配置信息
public String getCacheConfig(String name) {
return cache.get(name);
}
//测试main方法
public static void main(String[] args) throws Exception {
Config config = new Config();
// 模拟一个配置项,实际生产中会在系统初始化时从配置文件中加载进来
config.save("timeout", "2000");
for (int i = 0; i < 100; i++) {
System.out.println(config.getCacheConfig("timeout"));
TimeUnit.SECONDS.sleep(3);//每隔三秒打印一次
}
}
}
注:希望大家技术越来越好
来源:CSDN
作者:Ah_le
链接:https://blog.csdn.net/qq_30609633/article/details/103463867