前言
因为公司最近项目原因正好用到了《分布式任务调度平台XXL-JOB》,项目结束打算看看他的源码,发现他还依赖于 《分布式服务框架XXL-RPC》,于是我决定先看XXL-RPC。但当我正准备看的时候,我发现XXL-RPC依赖于 《分布式服务注册中心XXL-REGISTRY》,于是我决定先看XXL-REGISTRY
让我们先看看它自己怎么吹自己的
[1.1 概述]
XXL-REGISTRY 是一个轻量级分布式服务注册中心,拥有"轻量级、秒级注册上线、多环境、跨语言、跨机房"等特性。现已开放源代码,开箱即用。
[1.2 特性]
- 1、轻量级:基于DB与磁盘文件,只需要提供一个DB实例即可,无第三方依赖;
- 2、实时性:借助内部广播机制,新服务上线、下线,可以在1s内推送给客户端;
- 3、数据同步:注册中心会定期全量同步数据至磁盘文件,清理无效服务,确保服务数据实时可用;
- 4、性能:服务发现时仅读磁盘文件,性能非常高;服务注册、摘除时通过磁盘文件校验,防止重复注册操作;
- 5、扩展性:可方便、快速的横向扩展,只需保证服务注册中心配置一致即可,可借助负载均衡组件如Nginx快速集群部署;
- 6、多状态:服务内置三种状态:
- 正常状态=支持动态注册、发现,服务注册信息实时更新;
- 锁定状态=人工维护注册信息,服务注册信息固定不变;
- 禁用状态=禁止使用,服务注册信息固定为空;
- 7、跨语言:注册中心提供HTTP接口(RESTFUL 格式)供客户端实用,语言无关,通用性更强;
- 8、兼容性:项目立项之初是为XXL-RPC量身设计,但是不限于XXL-RPC使用。兼容支持任何服务框架服务注册实用,如dubbo、springboot等;
- 9、跨机房:得益于服务注册中心集群关系对等特性,集群各节点提供幂等的配置服务;因此,异地跨机房部署时,只需要请求本机房服务注册中心即可,实现异地多活;
- 10、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现 "服务注册中心" 产品开箱即用;
- 11、访问令牌(accessToken):为提升系统安全性,注册中心和客户端进行安全性校验,双方AccessToken匹配才允许通讯;
屁话不多,先把源码下下来,创建数据库,跑起来看看
界面简介,貌似可以手动注册服务,先注册一个玩一下。随便填写一下信息,保存
可以查看刚刚的注册信息
点击运行报表tab看到我刚刚注册的服务信息。
回到服务注册,再点击查看,发现我刚刚注册的地址没了,做下猜测:
因为我的注册服务之后没有和注册中心有任何互动,被注册中心自动下线了
界面大概就是这样,我们看看官方的使用说明
客户端maven依赖地址:
com.xuxueli
xxl-registry-client
${最新稳定版}
客户端API实用示例代码如下:
// 注册中心客户端(基础类)
XxlRegistryBaseClient registryClient = new XxlRegistryBaseClient("http://localhost:8080/xxl-registry-admin/", null, "xxl-rpc", "test");
// 注册中心客户端(增强类)
XxlRegistryClient registryClient = new XxlRegistryClient("http://localhost:8080/xxl-registry-admin/", null, "xxl-rpc", "test");
// 服务注册 & 续约:
List registryDataList = new ArrayList<>();
registryDataList.add(new XxlRegistryDataParamVO("service01", "address01"));
registryDataList.add(new XxlRegistryDataParamVO("service02", "address02"));
registryClient.registry(registryDataList);
// 服务摘除:
List registryDataList = new ArrayList<>();
registryDataList.add(new XxlRegistryDataParamVO("service01", "address01"));
registryDataList.add(new XxlRegistryDataParamVO("service02", "address02"));
registryClient.remove(registryDataList);
// 服务发现:
Set keys = new TreeSet<>();
keys.add("service01");
keys.add("service02");
Map> serviceData = registryClient.discovery(keys);
// 服务监控:
Set keys = new TreeSet<>();
keys.add("service01");
keys.add("service02");
registryClient.monitor(keys);
ok,那就从客户端代码入手。
首先需要创建一个registryClient
,先从XxlRegistryBaseClient
开始。看看他的构造函数。
public XxlRegistryBaseClient(String adminAddress, String accessToken, String biz, String env) {
this.adminAddress = adminAddress;
this.accessToken = accessToken;
this.biz = biz;
this.env = env;
// valid
if (adminAddress==null || adminAddress.trim().length()==0) {
throw new RuntimeException("xxl-registry adminAddress empty");
}
if (biz==null || biz.trim().length()<4 || biz.trim().length()>255) {
throw new RuntimeException("xxl-registry biz empty Invalid[4~255]");
}
if (env==null || env.trim().length()<2 || env.trim().length()>255) {
throw new RuntimeException("xxl-registry biz env Invalid[2~255]");
}
// parse
adminAddressArr = new ArrayList<>();
if (adminAddress.contains(",")) {
adminAddressArr.addAll(Arrays.asList(adminAddress.split(",")));
} else {
adminAddressArr.add(adminAddress);
}
}
就是一些简单的校验和设值,没什么看头。接下来看看服务的注册和续约。
/**
* registry
*
* @param registryDataList
* @return
*/
public boolean registry(List registryDataList){
// valid
if (registryDataList==null || registryDataList.size()==0) {
throw new RuntimeException("xxl-registry registryDataList empty");
}
for (XxlRegistryDataParamVO registryParam: registryDataList) {
if (registryParam.getKey()==null || registryParam.getKey().trim().length()<4 || registryParam.getKey().trim().length()>255) {
throw new RuntimeException("xxl-registry registryDataList#key Invalid[4~255]");
}
if (registryParam.getValue()==null || registryParam.getValue().trim().length()<4 || registryParam.getValue().trim().length()>255) {
throw new RuntimeException("xxl-registry registryDataList#value Invalid[4~255]");
}
}
// pathUrl
String pathUrl = "/api/registry";
// param
XxlRegistryParamVO registryParamVO = new XxlRegistryParamVO();
registryParamVO.setAccessToken(this.accessToken);
registryParamVO.setBiz(this.biz);
registryParamVO.setEnv(this.env);
registryParamVO.setRegistryDataList(registryDataList);
String paramsJson = BasicJson.toJson(registryParamVO);
// result
Map respObj = requestAndValid(pathUrl, paramsJson, 5);
return respObj!=null?true:false;
}
根据下方requestAndValid代码可知,这是一个发送http请求的代码,那么registry
方法,除了一些校验,其实就是向服务器发送了一个httppost请求。
private Map requestAndValid(String pathUrl, String requestBody, int timeout){
for (String adminAddressUrl: adminAddressArr) {
String finalUrl = adminAddressUrl + pathUrl;
// request
String responseData = BasicHttpUtil.postBody(finalUrl, requestBody, timeout);
if (responseData == null) {
return null;
}
// parse resopnse
Map resopnseMap = null;
try {
resopnseMap = BasicJson.parseMap(responseData);
} catch (Exception e) { }
// valid resopnse
if (resopnseMap==null
|| !resopnseMap.containsKey("code")
|| !"200".equals(String.valueOf(resopnseMap.get("code")))
) {
logger.warn("XxlRegistryBaseClient response fail, responseData={}", responseData);
return null;
}
return resopnseMap;
}
return null;
}
那么直接看看注册中心源码的这个 /api/registry
是干什么的吧
跳过controller的一些校验,直接进入service
@Override
public ReturnT registry(String accessToken, String biz, String env, List registryDataList) {
// valid
if (this.accessToken!=null && this.accessToken.trim().length()>0 && !this.accessToken.equals(accessToken)) {
return new ReturnT(ReturnT.FAIL_CODE, "AccessToken Invalid");
}
if (biz==null || biz.trim().length()<4 || biz.trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Biz Invalid[4~255]");
}
if (env==null || env.trim().length()<2 || env.trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Env Invalid[2~255]");
}
if (registryDataList==null || registryDataList.size()==0) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry DataList Invalid");
}
for (XxlRegistryData registryData: registryDataList) {
if (registryData.getKey()==null || registryData.getKey().trim().length()<4 || registryData.getKey().trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry Key Invalid[4~255]");
}
if (registryData.getValue()==null || registryData.getValue().trim().length()<4 || registryData.getValue().trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry Value Invalid[4~255]");
}
}
// fill + add queue
for (XxlRegistryData registryData: registryDataList) {
registryData.setBiz(biz);
registryData.setEnv(env);
}
registryQueue.addAll(registryDataList);
return ReturnT.SUCCESS;
}
除了一些校验,最关键的出现了!
registryQueue.addAll(registryDataList);
注册数据被塞进了一个队列里面。看看这个队列的定义:
private volatile LinkedBlockingQueue registryQueue = new LinkedBlockingQueue();
也就是说,我们注册(续约)服务的时候,只是把信息放入了一个队列。那么稍微动动 脑子就知道,当出队的时候,就是真正处理数据的时候。我们用IDE搜一搜这个队列的相关操作代码看看。
搜索结果激动人心,只有一个地方用了take
,一个地方用了addAll
(就是上面提到的),其他没有任何调用。
那就直接看看take出来数据做了什么事情吧。
ps:为了方便理解代码,先分看下数据库
在xxl_registry表中,biz,env,key一起构成了唯一主键,所有服务的值在data中维护成数组的形式存在
在xxl_registry_data表中,biz
,env
,key
,value
构成了唯一主键,每条数据代表了一个服务的注册信息,updateTime表示服务注册的时间。
消息表,记录服务变化信息
经验老道的程序员已经发现afterPropertiesSet
这个方法。
在afterPropertiesSet
中开了10个线程,每个线程做的事情就是:
- 从
registryQueue
里面取出一条服务注册数据 - 向数据库更新或新增一条
xxl_registry_data
数据(这个表主要用来记录某个服务最后的注册(续约)时间) - 从磁盘读取记录该服务的文件数据
3.1. 如果磁盘读取不到相应文件,则进入checkRegistryDataAndSendMessage
方法,把xxl_registry_data
中所有的value组成json array,对比xxl_registry
中的address
看是否一致。
3.1.1.xxl_registry
中不存在相应服务信息,则新增一条服务数据
3.1.2.xxl_registry
中存在相应服务,但服务address和jsonArray不一致,则更新xxl_registry
数据使其和xxl_registry_data
中的数据一致
3.1.3. 当数据不一致时,会向xxl_registry_message
中插入一条消息数据
3.2. 读取到了相应服务的信息,但status
不是0,则不做任何操作
3.3. 读取到了相应服务的信息,且包含了刚刚取出来的服务注册数据,则不做任何操作
@Override
public void afterPropertiesSet() throws Exception {
// valid
if (registryDataFilePath==null || registryDataFilePath.trim().length()==0) {
throw new RuntimeException("xxl-registry, registryDataFilePath empty.");
}
/**
* registry registry data (client-num/10 s)
*/
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
while (!executorStoped) {
try {
XxlRegistryData xxlRegistryData = registryQueue.take();
if (xxlRegistryData !=null) {
// refresh or add
int ret = xxlRegistryDataDao.refresh(xxlRegistryData);
if (ret == 0) {
xxlRegistryDataDao.add(xxlRegistryData);
}
// valid file status
XxlRegistry fileXxlRegistry = getFileRegistryData(xxlRegistryData);
if (fileXxlRegistry == null) {
// go on
} else if (fileXxlRegistry.getStatus() != 0) {
continue; // "Status limited."
} else {
if (fileXxlRegistry.getDataList().contains(xxlRegistryData.getValue())) {
continue; // "Repeated limited."
}
}
// checkRegistryDataAndSendMessage
checkRegistryDataAndSendMessage(xxlRegistryData);
}
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
}
}
});
}
}
/**
* update Registry And Message
*/
private void checkRegistryDataAndSendMessage(XxlRegistryData xxlRegistryData){
// data json
List xxlRegistryDataList = xxlRegistryDataDao.findData(xxlRegistryData.getBiz(), xxlRegistryData.getEnv(), xxlRegistryData.getKey());
List valueList = new ArrayList<>();
if (xxlRegistryDataList!=null && xxlRegistryDataList.size()>0) {
for (XxlRegistryData dataItem: xxlRegistryDataList) {
valueList.add(dataItem.getValue());
}
}
String dataJson = JacksonUtil.writeValueAsString(valueList);
// update registry and message
XxlRegistry xxlRegistry = xxlRegistryDao.load(xxlRegistryData.getBiz(), xxlRegistryData.getEnv(), xxlRegistryData.getKey());
boolean needMessage = false;
if (xxlRegistry == null) {
xxlRegistry = new XxlRegistry();
xxlRegistry.setBiz(xxlRegistryData.getBiz());
xxlRegistry.setEnv(xxlRegistryData.getEnv());
xxlRegistry.setKey(xxlRegistryData.getKey());
xxlRegistry.setData(dataJson);
xxlRegistryDao.add(xxlRegistry);
needMessage = true;
} else {
// check status, locked and disabled not use
if (xxlRegistry.getStatus() != 0) {
return;
}
if (!xxlRegistry.getData().equals(dataJson)) {
xxlRegistry.setData(dataJson);
xxlRegistryDao.update(xxlRegistry);
needMessage = true;
}
}
if (needMessage) {
// sendRegistryDataUpdateMessage (registry update)
sendRegistryDataUpdateMessage(xxlRegistry);
}
}
总结一下,服务注册的大致流程:
- 通过httppost请求,客户端把服务注册信息塞入注册中心的注册队列
- 注册中心后台有10个线程会从注册队列取出注册数据
- 同步注册信息到xxl_registry
- 发消息
嗯?是不是觉得很奇怪。注册只干这些事?不是磁盘操作吗?xxl_registry_data中的数据一直留着吗?消息什么时候处理?太久服务没有发出服务续约/心跳,服务不会自动下线吗?
这些问题先放一放,先看看其他几个接口吧
既然有服务注册,那就有服务移除,先看移除接口。
ps:重复的逻辑就略过了
@Override
public ReturnT remove(String accessToken, String biz, String env, List registryDataList) {
// valid
if (this.accessToken!=null && this.accessToken.trim().length()>0 && !this.accessToken.equals(accessToken)) {
return new ReturnT(ReturnT.FAIL_CODE, "AccessToken Invalid");
}
if (biz==null || biz.trim().length()<4 || biz.trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Biz Invalid[4~255]");
}
if (env==null || env.trim().length()<2 || env.trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Env Invalid[2~255]");
}
if (registryDataList==null || registryDataList.size()==0) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry DataList Invalid");
}
for (XxlRegistryData registryData: registryDataList) {
if (registryData.getKey()==null || registryData.getKey().trim().length()<4 || registryData.getKey().trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry Key Invalid[4~255]");
}
if (registryData.getValue()==null || registryData.getValue().trim().length()<4 || registryData.getValue().trim().length()>255) {
return new ReturnT(ReturnT.FAIL_CODE, "Registry Value Invalid[4~255]");
}
}
// fill + add queue
for (XxlRegistryData registryData: registryDataList) {
registryData.setBiz(biz);
registryData.setEnv(env);
}
removeQueue.addAll(registryDataList);
return ReturnT.SUCCESS;
}
这是一段似曾相识的代码,只是把服务信息放进了另一个队列,removeQueue
private volatile LinkedBlockingQueue
ok,按照注册的思路,我们看看针对removeQueue是不是也有10个线程从中反复取数据呢?
/**
* remove registry data (client-num/start-interval s)
*/
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
while (!executorStoped) {
try {
XxlRegistryData xxlRegistryData = removeQueue.take();
if (xxlRegistryData != null) {
// delete
xxlRegistryDataDao.deleteDataValue(xxlRegistryData.getBiz(), xxlRegistryData.getEnv(), xxlRegistryData.getKey(), xxlRegistryData.getValue());
// valid file status
XxlRegistry fileXxlRegistry = getFileRegistryData(xxlRegistryData);
if (fileXxlRegistry == null) {
// go on
} else if (fileXxlRegistry.getStatus() != 0) {
continue; // "Status limited."
} else {
if (!fileXxlRegistry.getDataList().contains(xxlRegistryData.getValue())) {
continue; // "Repeated limited."
}
}
// checkRegistryDataAndSendMessage
checkRegistryDataAndSendMessage(xxlRegistryData);
}
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
}
}
});
}
果不其然,代码几乎和注册时候的一一对应,只是把向xxl_registry_data
中插入数据变成了删除数据。
那么,服务注册相关的接口已经看完了。(没错,只有两个)
既然有服务注册,就有服务发现,有提供方,有消费方才是个正常的系统!
那接下来就看看怎么发现已经注册的服务!
先看客户端代码
// 服务发现:
Set keys = new TreeSet<>();
keys.add("service01");
keys.add("service02");
Map> serviceData = registryClient.discovery(keys);
很简单,就是把想要发现的服务的key加进了参数里面
那再看看注册中心的代码
@Override
public ReturnT
// get
public XxlRegistry getFileRegistryData(XxlRegistryData xxlRegistryData){
// fileName
String fileName = parseRegistryDataFileName(xxlRegistryData.getBiz(), xxlRegistryData.getEnv(), xxlRegistryData.getKey());
// read
Properties prop = PropUtil.loadProp(fileName);
if (prop!=null) {
XxlRegistry fileXxlRegistry = new XxlRegistry();
fileXxlRegistry.setData(prop.getProperty("data"));
fileXxlRegistry.setStatus(Integer.valueOf(prop.getProperty("status")));
fileXxlRegistry.setDataList(JacksonUtil.readValue(fileXxlRegistry.getData(), List.class));
return fileXxlRegistry;
}
return null;
}
仔细看看代码非常简单,就是从注册中心的本地磁盘读取服务信息,然后直接返回。也就是说,服务发现只会和磁盘产生交互,和数据库无关。和前面它自己吹嘘的一样。
那么问题又来了,注册中心是怎么保证磁盘文件的实时性和一直性的呢?还是老样子,这个问题放一放,先把最后一个客户端接口看完!
// 服务监控:
Set keys = new TreeSet<>();
keys.add("service01");
keys.add("service02");
registryClient.monitor(keys);
去注册中心看看
@Override
public DeferredResult> monitor(String accessToken, String biz, String env, List keys) {
// init
DeferredResult deferredResult = new DeferredResult(30 * 1000L, new ReturnT<>(ReturnT.SUCCESS_CODE, "Monitor timeout, no key updated."));
// valid
if (this.accessToken!=null && this.accessToken.trim().length()>0 && !this.accessToken.equals(accessToken)) {
deferredResult.setResult(new ReturnT<>(ReturnT.FAIL_CODE, "AccessToken Invalid"));
return deferredResult;
}
if (biz==null || biz.trim().length()<4 || biz.trim().length()>255) {
deferredResult.setResult(new ReturnT<>(ReturnT.FAIL_CODE, "Biz Invalid[4~255]"));
return deferredResult;
}
if (env==null || env.trim().length()<2 || env.trim().length()>255) {
deferredResult.setResult(new ReturnT<>(ReturnT.FAIL_CODE, "Env Invalid[2~255]"));
return deferredResult;
}
if (keys==null || keys.size()==0) {
deferredResult.setResult(new ReturnT<>(ReturnT.FAIL_CODE, "keys Invalid."));
return deferredResult;
}
for (String key: keys) {
if (key==null || key.trim().length()<4 || key.trim().length()>255) {
deferredResult.setResult(new ReturnT<>(ReturnT.FAIL_CODE, "Key Invalid[4~255]"));
return deferredResult;
}
}
// monitor by client
for (String key: keys) {
String fileName = parseRegistryDataFileName(biz, env, key);
List deferredResultList = registryDeferredResultMap.get(fileName);
if (deferredResultList == null) {
deferredResultList = new ArrayList<>();
registryDeferredResultMap.put(fileName, deferredResultList);
}
deferredResultList.add(deferredResult);
}
return deferredResult;
}
看看代码,初始化了一个DeferredResult
DeferredResult deferredResult = new DeferredResult(30 * 1000L, new ReturnT<>(ReturnT.SUCCESS_CODE, "Monitor timeout, no key updated."));
DeferredResult
是spring-web包提供的一个异步响应对象,下面给出一些点单的sample
@GetMapping("deferredResultTest")
@ResponseBody
public DeferredResult test(@RequestParam("timeout") final Long timeout) throws InterruptedException {
final DeferredResult result = new DeferredResult<>(5L * 1000, "timeout");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("process start.....");
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
result.setResult("done");
System.out.println("process dnoe......");
}
}).start();
result.onTimeout(new Runnable() {
@Override
public void run() {
System.out.println("timeout");
result.setResult("timeout");
}
});
System.out.println("return result");
return result;
}
请求 http://localhost:8080/xxl-registry-admin/registry/deferredResultTest?timeout=3 时,返回done,控制台打印
process start.....
return result
process dnoe......
请求 http://localhost:8080/xxl-registry-admin/registry/deferredResultTest?timeout=8 时,返回timeout,控制台打印
return result
process start.....
timeout
process dnoe......
回到原来话题,monitor初始化了一个超时时间30秒的DeferredResult
,超时就返回一个no key update的response对象
private Map
然后去registryDeferredResultMap
这个map里找注册信息,把刚刚初始化的DeferredResult加入这个map中,然后就直接return了这个DeferredResult对象。
根据我们对DeferredResult对象的理解,这个对象在30秒内没处理完则会返回一个success对象,那么30秒内处理玩会发生什么呢?让我们搜一搜registryDeferredResultMap
,肯定在其他地方做了处理!
搜索结果表示只有一处
// set
public String setFileRegistryData(XxlRegistry xxlRegistry){
// fileName
String fileName = parseRegistryDataFileName(xxlRegistry.getBiz(), xxlRegistry.getEnv(), xxlRegistry.getKey());
// valid repeat update
Properties existProp = PropUtil.loadProp(fileName);
if (existProp != null
&& existProp.getProperty("data").equals(xxlRegistry.getData())
&& existProp.getProperty("status").equals(String.valueOf(xxlRegistry.getStatus()))
) {
return new File(fileName).getPath();
}
// write
Properties prop = new Properties();
prop.setProperty("data", xxlRegistry.getData());
prop.setProperty("status", String.valueOf(xxlRegistry.getStatus()));
PropUtil.writeProp(prop, fileName);
logger.info(">>>>>>>>>>> xxl-registry, setFileRegistryData: biz={}, env={}, key={}, data={}"
, xxlRegistry.getBiz(), xxlRegistry.getEnv(), xxlRegistry.getKey(), xxlRegistry.getData());
// brocast monitor client
List deferredResultList = registryDeferredResultMap.get(fileName);
if (deferredResultList != null) {
registryDeferredResultMap.remove(fileName);
for (DeferredResult deferredResult: deferredResultList) {
deferredResult.setResult(new ReturnT<>(ReturnT.SUCCESS_CODE, "Monitor key update."));
}
}
return new File(fileName).getPath();
}
看完代码可知,在调用setFileRegistryData
方法之后,通过和磁盘里注册数据的对比,如果有变化,则DeferredResult
就会被setResult
。那问题又来了,setFileRegistryData
这个方法是什么时候被调用呢?让我们再搜一搜!又是他!afterPropertiesSet
!总过有两个地方用到了setFileRegistryData
,先看第一个线程。这个线程每秒钟会运行一次while里面的代码。
/**
* broadcase new one registry-data-file (1/1s)
*
* clean old message (1/10s)
*/
executorService.execute(new Runnable() {
@Override
public void run() {
while (!executorStoped) {
try {
// new message, filter readed
List messageList = xxlRegistryMessageDao.findMessage(readedMessageIds);
if (messageList!=null && messageList.size()>0) {
for (XxlRegistryMessage message: messageList) {
readedMessageIds.add(message.getId());
if (message.getType() == 0) { // from registry、add、update、deelete,ne need sync from db, only write
XxlRegistry xxlRegistry = JacksonUtil.readValue(message.getData(), XxlRegistry.class);
// process data by status
if (xxlRegistry.getStatus() == 1) {
// locked, not updated
} else if (xxlRegistry.getStatus() == 2) {
// disabled, write empty
xxlRegistry.setData(JacksonUtil.writeValueAsString(new ArrayList()));
} else {
// default, sync from db (aready sync before message, only write)
}
// sync file
setFileRegistryData(xxlRegistry);
}
}
}
// clean old message;
if ( (System.currentTimeMillis()/1000) % registryBeatTime ==0) {
xxlRegistryMessageDao.cleanMessage(registryBeatTime);
readedMessageIds.clear();
}
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
}
}
});
先去xxl_registry_message
表里找没被消费的消息
注意,这里代码有个坑,这个
readedMessageIds
这个参数最后在sql里面的体现是not in
,所以是,找到没有被消费过的message
如果有没有被消费,则做下面处理:
- 把消息id加入到已消费list
readedMessageIds
里 - 如果服务被禁用,把服务的注册data数据清空
- 调用
setFileRegistryData
方法同步数据到磁盘,并响应给监听中的客户端 - 每隔BeatTime(10秒),从xxl_registry_message删除BeatTime(10秒)之前的数据。清空readedMessageIds列表
那么在看第二个线程,第二个线程是BeatTime(10秒)运行一次while里面的代码
/**
* clean old registry-data (1/10s)
*
* sync total registry-data db + file (1+N/10s)
*
* clean old registry-data file
*/
executorService.execute(new Runnable() {
@Override
public void run() {
while (!executorStoped) {
// align to beattime
try {
long sleepSecond = registryBeatTime - (System.currentTimeMillis()/1000)%registryBeatTime;
if (sleepSecond>0 && sleepSecond registryDataFileList = new ArrayList<>();
List registryList = xxlRegistryDao.pageList(offset, pagesize, null, null, null);
while (registryList!=null && registryList.size()>0) {
for (XxlRegistry registryItem: registryList) {
// process data by status
if (registryItem.getStatus() == 1) {
// locked, not updated
} else if (registryItem.getStatus() == 2) {
// disabled, write empty
String dataJson = JacksonUtil.writeValueAsString(new ArrayList());
registryItem.setData(dataJson);
} else {
// default, sync from db
List xxlRegistryDataList = xxlRegistryDataDao.findData(registryItem.getBiz(), registryItem.getEnv(), registryItem.getKey());
List valueList = new ArrayList();
if (xxlRegistryDataList!=null && xxlRegistryDataList.size()>0) {
for (XxlRegistryData dataItem: xxlRegistryDataList) {
valueList.add(dataItem.getValue());
}
}
String dataJson = JacksonUtil.writeValueAsString(valueList);
// check update, sync db
if (!registryItem.getData().equals(dataJson)) {
registryItem.setData(dataJson);
xxlRegistryDao.update(registryItem);
}
}
// sync file
String registryDataFile = setFileRegistryData(registryItem);
// collect registryDataFile
registryDataFileList.add(registryDataFile);
}
offset += 1000;
registryList = xxlRegistryDao.pageList(offset, pagesize, null, null, null);
}
// clean old registry-data file
cleanFileRegistryData(registryDataFileList);
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
try {
TimeUnit.SECONDS.sleep(registryBeatTime);
} catch (Exception e) {
if (!executorStoped) {
logger.error(e.getMessage(), e);
}
}
}
}
});
}
- 一开始做了一下时间对其到BeatTime(10秒)的操作(比如现在是10:10:03,那么会休眠7秒,对其到10:10:10)
- 清除
xxl_registry_data
表中30秒前的数据 - 从
xxl_registry
中循环取1000条数据,
3.1. 如果服务被锁定,则什么都不做
3.2. 如果服务被禁用,则设置data为空数组
3.3. 如果正常,则从xxl_registry_data
表取出相对服务的所有value组成list,更新回xxl_registry
表
3.4. 同步数据到磁盘,并把文件路径添加到registryDataFileList
里面 - 清除不在
registryDataFileList
里面的文件
看完这段代码就知道,这就是前面所说的,10秒钟全量同步数据。
看到这里,monitor的大致流程已经比较清晰
- 设置DeferredResult,
- 等待30秒,30秒内服务没有变化,则在30秒的时候返回success。
- 30秒内,如果【每秒检查message消息的线程】发现有变化,则会立即返回success
- 30秒内,如果【每10秒全量同步线程】发现有变化,则会立即返回success
所以,客户端的视角就是,服务信息有变化,则离开拿到monitor方法的返回,没变化则30秒的时候拿到返回值。
既然服务消费者的代码已经比较清楚,那再回头看看刚刚服务生产者的代码留下了的疑问。
注册只干这些事?不是磁盘操作吗?
消费者确实是磁盘操作,服务生产者会同事维护db和磁盘数据
xxl_registry_data
中的数据一直留着吗?
3倍心跳之前的数据,会在【每10秒全量同步线程】中,被删除
消息什么时候处理?
【每秒检查message消息的线程】
太久服务没有发出服务续约/心跳,服务不会自动下线吗?
会在【每10秒全量同步线程】中data被清空,所以需要服务生产者每隔一段时间就注册(续约)一次(就是重新调一次registry方法)。XxlRegistryClient
提供了后台线程自动注册(续约)
是个比较简单的生产者消费者模型,而且大多数的疑问都被解决了。
那么我们重新梳理一遍这个服务中心的工作流程:
- 服务生产者通过registry方法向注册中心注册(向
registryQueue
队列添加数据)- 后台线程会定时扫描
registryQueue
,并更新数据到db,发送消息(消息会被【每秒检查message消息的线程】消费,然后同步数据到磁盘)- 服务生产者通过remove方法向注册中心移除服务(逻辑和注册一致)
- 服务消费者从磁盘读取服务生产者数据
- 10秒钟会有一次全量数据同步
附上官方架构图一张
让我们在回头看看他吹嘘的功能
1、轻量级:基于DB与磁盘文件,只需要提供一个DB实例即可,无第三方依赖;
2、实时性:借助内部广播机制,新服务上线、下线,可以在1s内推送给客户端;
3、数据同步:注册中心会定期全量同步数据至磁盘文件,清理无效服务,确保服务数据实时可用;
4、性能:服务发现时仅读磁盘文件,性能非常高;服务注册、摘除时通过磁盘文件校验,防止重复注册操作;
5、扩展性:可方便、快速的横向扩展,只需保证服务注册中心配置一致即可,可借助负载均衡组件如Nginx快速集群部署;
6、多状态:服务内置三种状态:
正常状态=支持动态注册、发现,服务注册信息实时更新;
锁定状态=人工维护注册信息,服务注册信息固定不变;
禁用状态=禁止使用,服务注册信息固定为空;
7、跨语言:注册中心提供HTTP接口(RESTFUL 格式)供客户端实用,语言无关,通用性更强;
8、兼容性:项目立项之初是为XXL-RPC量身设计,但是不限于XXL-RPC使用。兼容支持任何服务框架服务注册实用,如dubbo、springboot等;
9、跨机房:得益于服务注册中心集群关系对等特性,集群各节点提供幂等的配置服务;因此,异地跨机房部署时,只需要请求本机房服务注册中心即可,实现异地多活;
10、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现 "服务注册中心" 产品开箱即用;
11、访问令牌(accessToken):为提升系统安全性,注册中心和客户端进行安全性校验,双方AccessToken匹配才允许通讯;
除了5,8,9,10,11,其他的已经在刚才的代码阅读中得到验证。
8,10,11略过,不是这次阅读源码的重点。
我们还剩下最后一个疑惑,也就是多服务中心的时候怎么所有服务中心的数据一致。
先看看官方怎么说的
服务注册中心集群(可选)
服务注册中心支持集群部署,提升消息系统容灾和可用性。
集群部署时,几点要求和建议:
- DB配置保持一致;
- 登陆账号配置保持一致;
- 建议:推荐通过nginx为集群做负载均衡,分配域名。访问、客户端使用等操作均通过该域名进行。
4.3 跨机房(异地多活)
得益于服务注册中心集群关系对等特性,集群各节点提供幂等的服务注册服务;因此,异地跨机房部署时,> 只需要请求本机房服务注册中心即可,实现异地多活;
举个例子:比如机房A、B 内分别部署服务注册中心集群节点。即机房A部署 a1、a2 两个服务注册中心服务节点,机房B部署 b1、b2 两个服务注册中心服务节点;
那么各机房内应用只需要请求本机房内部署的服务注册中心节点即可,不需要跨机房调用。即机房A内业务应用请求 a1、a2 获取配置、机房B内业务应用 b1、b2 获取配置。
这种跨机房部署方式实现了配置服务的 "异地多活",拥有以下几点好处:
- 1、注册服务响应更快:注册请求本机房内搞定;
- 2、注册服务更稳定:注册请求不需要跨机房,不需要考虑复杂的网络情况,更加稳定;
- 2、容灾性:即使一个机房内服务注册中心全部宕机,仅会影响到本机房内应用加载服务,其他机房不会受到影响。
4.4 一致性
类似 Raft 方案,更轻量级、稳定;
- Raft:Leader统一处理变更操作请求,一致性协议的作用具化为保证节点间操作日志副本(log replication)一致,以term作为逻辑时钟(logical clock)保证时序,节点运行相同状态机(state machine)得到一致结果。
- xxl-registry:
- Leader(统一处理分发变更请求):DB消息表(仅变更时产生消息,消息量较小,而且消息轮训存在间隔,因此消息表压力不会太大;);
- state machine(顺序操作日志副本并保证结果一直):顺序消费消息,保证本地数据一致,并通过周期全量同步进一步保证一致性;
吓得我赶紧看了下Raft方案。看完之后,我就像,这系统用到Raft方案了吗?
看起来非常高大上,异地多活,跨机房,容灾,其实通过代码阅读,就可以很明显看出来:这个系统是通过DB实例以及10秒一次的全量同步来保证一致性的。不管数据怎么变,数据库中的xxl_registry_data
表才是真正的注册数据。只要隔一段时间同步这个表中的数据,就行了。当然,前提是只有一个DB实例(其实不是也可以)
那么,这个注册中心的大致编程思想通过源码阅读结合官方文档,已经基本了解。
下一篇,我们来看看《分布式服务框架XXL-RPC》吧