一、需求
2017年底开始,有一个项目计划,建设云数据中心,然后基于数据开拓增值服务。
背景:略。
要建议云数据中心,第一个就是要先把源端的数据同步回来。源端就是第三方,向我们开放数据服务,我们可以根据我们的用户,把我们用户相关的信息从源端同步到云端。这种场景就好像是:开办信用卡的代理只能通过它的客户(信用卡申请人)授权,才能查看客户的信贷信息。
源端的主要约束,除了上述的外,还有:
1) 只能在每个月的16号到月底可以同步。
2) 每次只能同步一种数据(据源端方面的人所说,一种数据其实就是对应他们的一张表)。数据有700多种,也就是有700多个数据接口,分三类,A类有20多种数据,B类有10多种数据,C类有700多种数据。
3) 只接受我们的10个并发请求。
4) 都是日志类的数据,数据接口的每次调用必须设置时间标识。时间的跨度越大,返回的数据量就越大。
我们的主要约束(要求):
1) 数据保存在公有云,数据库是MySQL。
2) 由于某种原因,我们必须采用定时任务方式同步数据。定时方式,我们又称为任务策略。
3) 不提供操作界面。
下面,就数据同步子系统的建设进行分析与设计。
二、分析与设计
1、 流程分析与设计:
1)初步分析与结果:
因为我们必须根据我们手上的用户信息,才能把源端的数据同步回来,因此,我们必须有个初始化步骤,把我们用户关键信息还有源端的数据种类导入来。接着,就是启动任务将数据同步回来,再将同步回来的数据根据数据种类不同而分开保存(也就是说,每个源端的数据种类,在云端都有一个对应的数据表)。流程是这样的:
每次不能同步太多的数据回来,因此,我们决定按数据类型、数据种类、时间标识分批将数据同步回来,另外,将原来一个流程拆分为初始化与数据同步(任务)两个流程。
初始化流程:
数据同步(任务)流程:
于是,得出粗略的第一个业务建模版本:
“签约用户”、“待同步数据种类列表”与“源数据获取”有关联,为了解耦,设计了一个新的业务对象“签约用户待同步数据”,也就是说,“签约用户待同步数据”是由“签约用户”和“待同步数据种类列表”组成。
“同步任务”是由“源数据获取”、“同步日志”、“任务策略”组成。第一版业务建模:
设计第一版本的接口与交互:
数据初始化流程,在系统初始化时发生,也就是签约用户数据与源数据种类的数据导入,计划手工完成,因此不设计接口;并且,即使不是手工导入,也不考虑接口设计,只关注关键的部分,也就是通过数据同步任务将数据取回来,于是设计了数据同步任务相关接口与交互,如下图:
2)迭代一
根据上述的接口设计,再优化业务模型:
增加了“调用上下文”业务对象,使任务与数据保存解耦。如下图所示:
因为已经打算将“数据保存”从“任务”同解耦出来,不过,在流程设计中还没有体现出来,当然接口设计中也没有体现出来,于是,重新设计数据保存(任务)的流程与接口。
任务流程:
数据保存流程:
任务与数据保存接口设计与交互:
可以看到,在上下文中执行任务并保存数据。
将关键数据持久化,设计关键的数据ER模型如下:
2、 功能设计:
系统不要求提供界面,我们主要实现三大功能:任务调度服务器,任务执行,数据同步(保存)。
细分。略。
3、 构架划分:
通过上述的分析,我们已经将业务流程与模型、业务接口都已经设计出来了。接着,我们可以考虑子系统(模块)的划分了。一般情况下,在业务分析过程中,对于一个熟悉开发技术的架构师来说,会一边脑补构架如何实现,会对业务进行切分(也就是分出子系统)。出于解耦目的,对于数据同步的任务调度器,我们当然希望是独立的与业务无关,它的作用就是发送一个通知启动任务;任务被触发后,调用接口获取数据;获取数据后,将数据保存到数据表。因此基本可以确定,系统分为三部分。逻辑架构如下图所示:
在数据获取成功后,就通过MQ把数据传到数据保存服务将数据持久化。
4、 再迭代
因为通过MQ解耦了,接口与交互的设计必须改进:
在数据同步方面,收到任务中发过来的消息(数据)后,保存在数据库表中:
5、 技术选型
关键技术:
任务调度服务器:选择开源的实现XXL-JOB
开发技术:JAVA
消息中间件:RabbitMQ
数据存储服务:DRDS四、接口定义:
关键接口定义。通过上一节的分析与设计,已产出了任务与数据保存接口,接着就是根据设计进行接口定义。
要定义的接口有:
触发任务、任务执行、记录任务日志、选择任务策略、用户待同步信息、同步数据、执行源数据获取请求、记录同步日志、记录最终失败日志、保存已获取到的数据、表数据保存、记录数据保存日志、消息发送、消息接收。
其中,触发任务与选择任务策略,已由XXL-JOB提供并已实现,那么,只需要定义其它的接口即可。
1) 任务执行
publicinterface ISyncTask {
/**任务执行接口:
* 向源端发起数据获取调用{@linkjob.excutor.api.sync.IDataSyncService#sync()},记录数据获取调用的日志{@linkjob.excutor.api.logs.IDataSyncLogService#log4DataSync(Object)},同时还要保存获得的数据通过MQ传到数据保存子系统的数据库中{@linkjob.excutor.api.persist.IDataSaveService#saveSyncData(java.io.Serializable)} 或者{@job.excutor.api.persist.IDataSaveService#saveSyncData(java.util.List)}。
* 初始化同步与增量同步,具有共通性,
* 因此,可将初始化同步与增量同步定义为同一个方法。
*
*
* @paramt java.io.Serializable, 是任务调度服务器传过来的参数(建议带上任务每次调用的UUID,这样可以实现任务结果的异步通知。)。
*@return java.io.Serializable, 自定义的数据结构体,数据体包括两部分:
* 1)任务的执行结果2)其它。
*/
AgetData4Save(T t);
}
2) 记录任务日志
public interfaceITaskExeLogService {
/**记录任务执行前日志。
*
* @paramk 任务每次执行的UUID,自动生成。
* @parammsg Object,日志信息
*@return Boolean 任务执行的结果,成功还是失败
*/
Boolean preExeLog(K k, Object msg);
/**记录任务执行后日志。
* 主要是记录任务执行后的结果。
*
* @paramk 任务每次执行的UUID,用于定位任务,写回结果。
* @parammsg Object,日志信息
*@return Boolean 任务执行的结果,成功还是失败
*/
Boolean afterExeLog(K k, Object msg);
}
3) 用户待同步信息
publicinterface IUserParamsService{
/**获取用户待同步信息。用于构造入参调用源数据服务接口。
* 用户待同步信息是保存在数据库中。
*@returnList, P自定义数据体,必须包括两方面信息:用户信息,那种源数据类型
*/
List getUserParams();
}
4) 同步数据
public interface IDataSyncService {
/**数据同步接口:
* 数据同步由源端数据获取{@linkjob.excutor.api.fetch.IDataFetchService#getDataFromOutside(java.io.Serializable)}、
* 同步日志{@linkjob.excutor.api.logs.IDataSyncLogService#log4DataSync(Object)}、数据保存{@linkjob.excutor.api.persist.IDataSaveService#saveSyncData(java.io.Serializable)} 或者 {@linkjob.excutor.api.persist.IDataSaveService#saveSyncData(java.util.List)},
* 三大部分组成。
* @paramparam Serializable 从任务上下文环境中传过来的参数,用于发送Remote请求获得数据。
*@return boolean
*/
boolean sync(java.io.Serializable params);
}
5) 执行源数据获取请求
public interface IDataFetchService {
/**从源端获取数据的接口:
* 向源端发起数据获取调用,记录数据获取调用的日志{@linkjob.excutor.api.logs.IDataSyncLogService#log4DataSync(Object)}。
* 待同步的信息从数据库中获取,接着根据这些信息通过远程调用获取局端数据。
*
*
* @paramt java.io.Serializable, 数据获取时的参数:
* 主要是从任务调用服务器带来的参数。建议带上任务每次调用的UUID,这样可以实现任务结果的异步通知。
*
*@return java.io.Serializable, 自定义的数据结构体,数据体包括:
* 从源端取回来的数据。
*/
AgetDataFromOutside(java.io.Serializable t);
}
6) 记录同步日志
publicinterface IDataSyncLogService {
/**记录数据同步请求的日志。
* @paramdataFetchParams Object, 发送数据获取请求的参数结构体,包括签约用户待同步信息。
*@return Boolean, 数据同步是成功还是失败
*/
Boolean log4DataSync(Object dataFetchParams);
}
7) 记录最终失败日志
publicinterface IFinalFailureLogService {
/**数据同步失败(包括数据获取与数据保存)最终失败,记录关键信息,
* 用于追查与人工介入处理。
*
* @paramfailureDetailsInfo 可用于追查与人工介入处理的信息,如:
* 签约用户、尖端数据种类、同步的时间节点,任务UUID等。
*/
void logSyncFailureLog(ObjectfailureDetailsInfo);
}
8) 保存已获取到的数据
public interfaceIDataSaveService {
/**将从源端取得的目标数据保存到本地数据库中。
* @paramt java.io.Serializable 数据表的entity
*@return Boolean 数据保存操作的结果,成功还是失败。
*/
BooleansaveSyncData(T t);
/**将从源端取得的目标数据并批量保存到本地数据库中。
* @paramlist List,数据表的entity的列表
*@return Boolean 数据保存操作的结果,成功还是失败。
*/
BooleansaveListSyncData(List list);
}
9) 表数据保存
publicinterface ITableSaveService{
/**将从源端取得的目标数据保存到本地数据库中。
* @paramt java.io.Serializable 数据表的entity
*@return Boolean 数据保存操作的结果,成功还是失败。
*/
boolean save();
/**将从源端取得的目标数据并批量保存到本地数据库中。
* @paramlist List,数据表的entity的列表
*@return Boolean 数据保存操作的结果,成功还是失败。
*/
BooleansaveList(List list);
}
10) 记录数据保存日志
publicinterface ITableSaveLogService{
/**
* @parammsg
*
*/
void saveLog(Object msg);
}
五、任务执行接口实现
由于任务是由记录任务日志、选择任务策略、用户待同步信息、同步数据组成;
而同步数据是由执行源数据获取请求、记录同步日志、记录最终失败日志组成;
保存已获取到的数据是由表数据保存、记录数据保存日志、记录最终失败日志组成。
1、 “同步数据”接口实现
publicclass DataSyncService implements IDataSyncService {
private IDataFetchService djDataFetchService;
private IDataSyncLogServicedjDataSyncLogService;
private IFinalFailureLogServicedjFinalFailureLogService;
private IDjDataSaveService djDataSaveService;
/* (non-Javadoc)
* @see job.excutor.api.sync.IDataSyncService#sync()
*/
@Override
public boolean sync(Serializable params) {
// TODO Auto-generated method stub
Listlist=this.getRequestParamsList(params);
for(Serializable param:list){
Serializableresult=djDataFetchService.getDataFromOutside(param);
booleansuccessFlag=djDataSyncLogService.log4DataSync(result);
if(!successFlag){//如果最终还是失败,记录日志,用于人工介入处理
djFinalFailureLogService.logSyncFailureLog(this.toFinalFailureLog(param,result));
return false;
}
successFlag=djDataSaveService.saveSyncData(toTableData(result));
if(!successFlag){//如果数据保存失败,记录日志,用于人工介入处理
djFinalFailureLogService.logSyncFailureLog(this.toFinalFailureLog(param,result));
return false;
}
}
return true;
}
/**数据获取最终失败,记录失败信息,使可用于人工介入处理:纠正错误,根据参数重新再发送请求。
* @paramparam Serializable 数据获取请求时所携带的参数
* @paramresult Serializable 数据获取返回的(失败)结果
*@return 失败日志数据结构体
*/
private Object toFinalFailureLog(Serializableparam, Serializable result){
//todo ...
return null;
}
/**将获取的数据待久化到数据表。
*
* @paramresult Serializable 数据获取返回的(成功)结果
* @returnList 表entity列表
*/
private ListtoTableData(Serializable result){
// todo ...
return null;
}
/**根据任务执行传入的参数,构造签约待同步数据列表,用于进行下一步的数据获取请求。
* @paramparams Serializable 任务执行传入的参数
*@return List
*/
private ListgetRequestParamsList(Serializable params){
// todo ...
return null;
}
}
2、 “任务执行”接口实现
publicclass DjSyncTask implements ISyncTask {
private IDataSyncService djDataSyncService;
private ITaskExeLogService djTaskExeLogService;
/* (non-Javadoc)
* @seejob.excutor.api.task.ISyncTask#getData4Save(java.io.Serializable)
*/
@Override
public AgetData4Save(Serializable t) {
// TODO Auto-generated method stub
Object preMsg=null; // toDo ...
djTaskExeLogService.preExeLog(getTaskUUID(t),preMsg);
booleansuccessFalg=djDataSyncService.sync(t);
djTaskExeLogService.afterExeLog(getTaskUUID(t),createMsg(successFalg));
// todo ...
// 构造结果消息数据体,并返回
return null;
}
private Object getTaskUUID(Serializable t){
//todo ...
return null;
}
private Object createMsg(boolean success){
return null;
}
}
其它的接口实现,略。
根据接口设计与业务建模,可以灵活地实现接口。如:数据同步接口可以看作是数据保存接口的代理,可以应用代理设计模块。数据保存,我只设计一个接口方法,而调用这个接口时,要根据源数据种类分别保存到700多张表中去,这里可以应用工厂模式。任务执行,在这个接口被调用的前后,我都会记录日志,那么这里可以应用模板模式。
六、接口调用,实现业务
接口调用,实现业务的执行是上下文业务对象
public class JobHandler {
private ISyncTask djSyncTask;
publicvoid execute(String param) throws Exception {
//TODO Auto-generated method stub
SerializabletaskResult=djSyncTask.getData4Save(param);
//todo...
}
}
注意,在业务建模中,上下文对象还调用了“数据同步”接口,不过由于数据获取与数据保存是在两个进程中,通过MQ通讯,因此,“数据同步”这个接口,并不是在JobHandler中被调用,而是在另一个上下文的实现类中实现(略)。