JD·config配置中心
主要功能:
提供动态的Key,Value配置读写服务,基于zookeeper的原子性与Watcher机制,保障了数据的一致性与时效性
优点:
1、二次检索的数据会注入到Jvm缓存中,增强了数据的检索效率
2、数据会定期copy到磁盘,当zk不可用时,保障了服务的可用性
3、内部自动zk失效重连
缺点:
1、占用客户端内存、磁盘空间
Spring配置项:
<bean id="configCenterClient" class="***.client.DefaultConfigCenterClient" init-method="init" destroy-method="destroy">
<!--配置中心地址-->
<property name="zkServers" value="${config.center.zkServers}" />
<!--本地快照-->
<property name="storeFilePath" value="${config.center.snapshotPath}"/>
<!--zk节点目录-->
<property name="zNodePath" value="${config.center.znode}" />
</bean>
Jar包class:
关键Class的功能
DefaultConfigCenterClient主要职责:
1、负责与外界交互,配置中心 Base类
2、负责zk数据变化时,调用者的注册、监听
3、负责与ZkClient交互数据并更新数据到Jvm缓存,提升检索效率
4、负责与zk失联时,从本地磁盘获得数据
ZkClient职责:
1、负责与zk直接交互(连接、重连、获取数据、注册监听)
2、负责监听zk数据变化、连接变化
3、负责维护zk监听列表&config配置列表
spring初始化工作
从以上代码基本判定初始化步骤:1、获取zk连接 2、初始化空数据同步监听 3、初始化快照同步监听
第一步:initZookeeper() 方法关键性代码:
DefaultConfigCenterClient类:
private void initZookeeper() {
//初始化参数
this.zkClient = new ZkClient(this.zkServers, this.sessionTimeout);
//注册监听,--后面有用
this.zkClient.subscribeZkConnectionListener(this);
try {
this.connectZk(2);
_logger.info("connect to zk successful!");
} catch (ConfigCenterException var2) {
_logger.error("connect to zk fail! ", var2);
}
}
//这里是真实的连接zk集群(客户端长连接),通过递归获得重试,2次
private void connectZk(int toTryCount) throws ConfigCenterException {
try {
this.zkClient.connect();
} catch (IOException var3) {
if (toTryCount <= 0) {
throw new ConfigCenterException("try " + toTryCount + " time, connect still fail! ", var3);
}
this.zkClient.close();
this.connectZk(toTryCount - 1);
}
}
第二步:failWatchMonitor初始化方法关键性代码:
FailWatchMonitor类:
//无数据时,间歇时间1分钟
private final long tickTime = 60000L;
//未检测到的key值Set
private Set<String> failWatchKeysQueue = new HashSet(1000);
//未检测到的key值Set
private DefaultConfigCenterClient client = null;
//重入锁保护failWatchKeysQueue的读、写线程安全
private final ReentrantLock _lock = new ReentrantLock();
//为_lock重入锁服务
private final Condition _notEmpty;
//线程方法,初始化时启动
public void run() {
//todo:3个while(true) 么看懂?
while(true) {
while(true) {
while(true) {
try {
//尝试拿到锁
if (this._lock.tryLock()) {
try {
//无数据时,休息10分钟
if (this.failWatchKeysQueue.isEmpty()) {
_logger.info("failWatchKeysQueue is empty, lockEmptyWorkQueue");
this._notEmpty.await(600000L, TimeUnit.MILLISECONDS);
continue;
}
//有数据时,更新数据
this.watchSets();
} catch (InterruptedException var7) {
_logger.error("lock error! " + var7.getMessage());
} catch (Exception var8) {
_logger.error("lock error!", var8);
} finally {
this._lock.unlock();
}
//更新周期:1分钟一次
sleep(60000L);
}
} catch (Exception var10) {
_logger.error("FailWatchMonitor error! ", var10);
}
}
}
}
}
private void watchSets() {
Iterator it = this.failWatchKeysQueue.iterator();
//遍历空数据list,不加重入锁可能会抛异常(遍历+删除)
while(it.hasNext()) {
String key = (String)it.next();
try {
//直接从zk中拿数据
String value = this.client.getDataFromZkAndWatch(key, 1);
//有数据场景
if (value != null) {
//加载数据至JVM
this.client.put(key, value);
//移除空值list
it.remove();
_logger.info("readData and Watch success! key=" + key + ", value = " + value);
}
} catch (Exception var4) {
if (_logger.isDebugEnabled()) {
_logger.debug(var4.getMessage());
}
}
}
}
以上代码可得出结论:
正常情况下,FailWatchMonitor线程会默认每隔1分钟更新数据至jvm缓存,为的是提升缓存命中率
第三步:snapshotTimer初始化方法关键性代码:
SnapshotTimer类:
//快照循环更新间隔时间:30分钟
public static final long DEFAULT_INTERVAL = 1800000L;
//快照同步数据对象
private SnapshotAble snapshotAble = null;
//快照操作对象
private SnapshotManager snapshotManager = null;
//
private long interval = 1800000L;
//是否开启快照功能标记
private volatile boolean stop = false;
public void run() {
while(!this.stop) {
try {
//更新间隔时间
sleep(this.interval);
_logger.info("snapshot work will execute!");
//直接从jvm缓存中获得数据
Map<String, String> snapshot = this.snapshotAble.getSnapshot();
Iterator it = snapshot.entrySet().iterator();
//
while(it.hasNext()) {
Entry<String, String> entry = (Entry)it.next();
String key = (String)entry.getKey();
String value = (String)entry.getValue();
this.snapshot(key, value);
if (this.stop) {
break;
}
}
_logger.info("snapshot work execute successful!");
} catch (Exception var6) {
}
}
}
// this.snapshot(key, value)具体实现
//先删除在更新,写使用PrintWriter字符输出流
public void storeSnapshot(String key, String snapshot) throws IOException {
File file = this.getTargetFile(key);
file.delete();
this.saveSnapshot(key, snapshot);
}
以上代码可得出结论:
SnapshotTimer线程会默认每隔30分钟更新jvm数据至快照
第四步(补充):zk Sync初始化方法关键性代码:
//这个方法是为了提升【调用Default类的客户端】注册数据变化监听的成功率
//init()代码块方法:
if (this.connected == 0) {
try {
_logger.info("begin to wait response of zk!");
this.doneSignal.await(10000L, TimeUnit.MILLISECONDS);
} catch (InterruptedException var3) {
}
}
//DefaultConfigCenterClient类方法:
//获得zk某客户端服务器连接时,后面会讲到
public void syncConnected() {
this.connected = 1;
this.doneSignal.countDown();
//监听,不确定注册的客户端和zk启动的先后顺序?,注册的时候已经调用了this.get(key)方法
this.reloadConfigChangedListeners();
_logger.info("zk connected");
}
//【调用Default类的客户端】通过此方法进行监听
public void registerConfigChangedListener(String key, ConfigChangedListener configChangedListener) {
this.registeredConfigChangedListeners.put(key, configChangedListener);
//开启zk客户端监听并将kv值注入jvm
this.get(key);
}
//具体监听的逻辑
private void reloadConfigChangedListeners() {
Iterator it = this.registeredConfigChangedListeners.keySet().iterator();
while(it.hasNext()) {
this.get((String)it.next());
}
}
以上代码可得出结论:
1、【调用Default类的客户端】注册监听时,会将结果存储到registeredConfigChangedListeners对象中,后续zk监听触发时调用
2、【调用Default类的客户端】注册监听与zk初始化都会调用一次this.get(key)方法来进行zk的注册监听以及将kv值注入jvm
获取数据
DefaultConfigCenterClient类:
//获得k对应的v
public String get(String key) {
if (StringUtils.isEmpty(key)) {
throw new IllegalArgumentException("key can not be empty!");
} else {
//jvm中获取数据
String value = (String)this.localCache.get(key);
//Failover模式或已拿到数据,直接返回数据
if (this.status != -1 && value == null) {
//注册key变化监听到zkClient
this.zkClient.subscribeDataChangeListener(this.generateZkPath(key), this);
//优先从zk中获取数据,允许失败1次
value = this.getDataFromZkAndWatch(key, 2);
if (value != null) {
_logger.info("getDataFromZkAndWatch success, key=" + key + ", value=" + value);
}
if (value == null) {
this.failWatchMonitor.register(key); //failWatchMonitor中failWatchKeysQueue数据就是从这来的
_logger.info("register into FailWatchMonitor, key=" + key);
//当zk也无数据时,从快照中获得数据
value = this.getDataFromSnapshot(key);
if (value != null) {
_logger.info("getDataFromSnapshot success, key=" + key + ", value=" + value);
} }
if (value != null) {
this.localCache.put(key, value);
}
return value;
} else {
return value;
} } }
以上代码可得出结论(get):
1、缓存中无数据时,zkClient注册数据监听,换句话说,缓存中有就不需要重复监听了
2、获取数据时,访问顺序:Jvm缓存 -> Zk -> 快照
通过ZK的Watcher机制实现监听
首先简单了解一下zk的watcher状态:
事件类型 | 触发条件 |
---|---|
KeeperState.Expired | 客户端和服务器在ticktime的时间周期内,是要发送心跳通知的。这是租约协议的一个实现。客户端发送request,告诉服务器其上一个租约时间,服务器收到这个请求后,告诉客户端其下一个租约时间是哪个时间点。当客户端时间戳达到最后一个租约时间,而没有收到服务器发来的任何新租约时间,即认为自己下线(此后客户端会废弃这次连接,并试图重新建立连接)。这个过期状态就是Expired状态 |
KeeperState.Disconnected | 就像上面那个状态所述,当客户端断开一个连接(可能是租约期满,也可能是客户端主动断开)这是客户端和服务器的连接就是Disconnected状态 |
KeeperState.SyncConnected | 一旦客户端和服务器的某一个节点建立连接,并完成一次version、zxid的同步,这时的客户端和服务器的连接状态就是SyncConnected |
KeeperState.AuthFailed | zookeeper客户端进行连接认证失败时,发生该状态 |
事件类型 | 触发条件 |
---|---|
EventType.NodeCreated | 当节点被创建时,触发 |
EventType.NodeChildrenChanged | 当节点的直接子节点被创建、被删除、子节点数据发生变更时,触发 |
.EventType.NodeDataChanged | 当节点的数据发生变更时,触发 |
EventType.NodeDeleted | 当节点被删除时,触发 |
EventType.None | zookeeper客户端的连接状态发生状态切换时,触发 |
ZkClient实现Watcher的process方法监听
public void process(WatchedEvent event) {
String path = event.getPath();
_logger.info("path : " + path + ", type :" + event.getType() + ", state = " + event.getState());
//数据变化的3种状态
//数据变化时,DefaultConfigCenterClient实现,可以重写
if (event.getType() == EventType.NodeDataChanged) {
this.fireDataChangedEvents(path);
//数据删除时,由基类(DefaultConfigCenterClient) Impl类实现
} else if (event.getType() == EventType.NodeDeleted) {
this.fireNodeDeleted(path);
//数据创建时,同上
} else if (event.getType() == EventType.NodeCreated) {
this.fireNodeCreated(path);
}
//连接状态变化的3种状态
Iterator i$;
ZkConnectionListener listenerZk;
//连接过期
if (event.getState() == KeeperState.Expired) {
this.reconnect();
i$ = this.registeredZkConnectionListeners.iterator();
while(i$.hasNext()) {
listenerZk = (ZkConnectionListener)i$.next();
listenerZk.expired();
}
//获得zk某客户端服务器连接时
} else if (path == null && event.getState() == KeeperState.SyncConnected) {
//更新zkClient key值监听
this.subscribeAll();
i$ = this.registeredZkConnectionListeners.iterator();
//通知Default注册对象做自己的事
while(i$.hasNext()) {
listenerZk = (ZkConnectionListener)i$.next();
listenerZk.syncConnected();
}
//连接失效时
} else if (event.getState() == KeeperState.Disconnected) {
i$ = this.registeredZkConnectionListeners.iterator();
while(i$.hasNext()) {
listenerZk = (ZkConnectionListener)i$.next();
listenerZk.disconnected();
}
}
}
数据变化时:
ZkClient类:
private void fireDataChangedEvents(String path) {
ZkDataListener listener = (ZkDataListener)this.registeredDataListeners.get(path);
if (listener != null) {
try {
//从zk中读取数据+继续监听
Object data = this.readData(path, (Stat)null, true);
//触发【调用Default类的客户端】监听方法
listener.handleDataChange(path, data);
} catch (ZkException var4) {
_logger.error(listener + " : ", var4);
} } }
数据增加+删除时:
ZkClient类:
private void fireNodeDeleted(String path) {
ZkDataListener listener = (ZkDataListener)this.registeredDataListeners.get(path);
if (listener != null) {
try {
//触发DefaultConfigCenterClient类方法,默认do nothing
listener.handleDataDeleted(path);
} catch (ZkException var4) {
_logger.error(listener + " : ", var4);
}
}
}
获得zk某客户端服务器连接时:
ZkClient类:
private void subscribeAll() {
_logger.info("subscribeAll");
//注册数据监听,担心遗漏,所以+synchronized
synchronized(this.registeredDataListeners) {
Iterator i$ = this.registeredDataListeners.keySet().iterator();
while(i$.hasNext()) {
String path = (String)i$.next();
this.watchForData(path);
}
}
}
zk连接失效时:
ZkClient类:
if (event.getState() == KeeperState.Expired) {
//重连
this.reconnect();
//触发DefaultConfigCenterClient类方法,默认do nothing
i$ = this.registeredZkConnectionListeners.iterator();
while(i$.hasNext()) {
listenerZk = (ZkConnectionListener)i$.next();
listenerZk.expired();
}
以上代码可得出结论:
1、zk数据或连接状态的变更都会触发:DefaultConfigCenterClient的对应方法
2、zk连接失效后会进行重连,获得一个新的zk连接
3、数据变化会更新jvm、重新开启数据监听,触发【调用Default类的客户端】方法
4、数据删除默认不触发任何事件?假设从配置中心干掉一条数据,缓存中的数据会一直存在?
5、数据新增默认不触发任何事件?实际是很难做到,但假设【调用Default类的客户端】先注册监听key1,后在配置中心新增key1,那么key1发生变化是无法监测到的,除非先调用一次get(key1)方法
6、从过期状态->重新连接->Sync状态,期间【调用Default类的客户端】监听会失效