系统功能概述:
我们的应用所面向的终端用户是医学人员,系统部署后,用户可以通过复杂的医学条件检索出符合条件的病人,称之为队列,然后定义变量,也就是再次查询每一个病人的具体信息,如年龄,性别,是否服药,检测的血压值是多少等等,有了这些数据,就可以做统计学方面的分析,得出一些医学结论。
业务需求和解释:
客户提出了一个应用多节点部署,并且操作同步的业务需求。这个客户是省市级的医院,其下还有乡镇,社区医院,应用要在每一家医院部署,数据库 Schema 也是一样的,只是每家医院的数据各自来源于自己的 HIS, EMR 等系统(我们部署前会从这些数据源抽取数据到我们应用依赖的数据库里去)。假设部署了 A,B,C,D,E 五家医院, A 作为研究项目的发起方,可以邀请 B,C,D 三家医院(任意家都行)共同做一个科学研究, 三家被邀请的医院都同意了, A 作为请求方就通过应用的界面进行操作, B,C,D 不需要人工干预, 就能完成步骤一模一样的科研,最终和 A 一样得出医学统计结果. A 也可以让 B,C,D 把病人的必要信息传过来,在自己的应用上做统计分析. 应用的初衷就是这些医院组成一个联盟,扩大数据库样本,有条件(数据不共享,即使共享也是脱敏后必需的信息共享)地进行科研.
实现要点:
1. 被邀请节点的业务操作和 A 节点同步, 节点间的通信是消息驱动还是直接 WebService 请求?
2. 请求被处理的时长可能相差 N 个数量级,怎么协同?
3. 如何保障通信的健壮,不丢失,且不会多次执行,即便多次执行,是否能够幂等?
正如业务功能概述里所说,系统功能大致分成了几个步骤,这些步骤都是强依赖的链路,任何一环出问题,都无法继续下去。虽然数据库 Schema 一样,但各医院的数据体量不一样,同样一个查询,各个应用的响应时间差别很大,有些查询,几秒到几十分钟的差别都是可能的。我们的实现的方案采用了本地消息表的方式,消息发送后等待反馈保证通信成功,健壮性则是通过消息的消费顺序和发送顺序一致,加上幂等操作。业务上,我们对一个科研请求涉及到的后台请求做整合,提炼出几个必须要进行同步的接口,使用 Spring AOP 进行监控,一旦接口被调用,就对数据库的业务表进行查询,检索出必需的数据,转成对象,序列化成 JSON。然后将接口信息,包括请求体JSON,发送时间等存到本地消息表。成功后,再通过 WebService 将消息发送到到被邀请的节点,被邀请节点返回一个反馈,然后自己消费消息。
技术难点和实现:
1. 消息的消费时长千差万别,发送方不可能等待接收方消费完消息再返回反馈。所以消息接收方在把消息存入本地库后,主线程立即返回反馈(HTTP响应),然后开一个子线程消费消息
2. 对请求方来说,人工操作,发送的消息时间先后顺序是确定的,那么消息要包含消息的发送时间,接收方消费消息以发送时间排序,逐个消费
3. 请求方如果超时时间内,没有收到反馈,消息就标记为失败,可以通过定时任务或者可视化界面手动触发消息的重发
4. 请求方重发消息,则一定是逐条发送,成功一个才发下一个,确保接收方收消息有序
5. 消息要包含一个 checkSum, 接收方通过这个属性判断消息是否接收过
6. 接收方消费消息时,要检查当前应用是否正在消费依赖消息,或者依赖的消息消费失败,然后决定是否可以消费刚刚接收到的消息
7. 消费消息前,要对相关表数据做一次清除,保证不会因为之前消息消费的失败,导致数据出错,进一步确保操作的幂等
8. 接收方如果消费消息失败,也是通过定时任务或者可视化界面手动触发消息的消费
由于消息链路的强依赖,各个消息之前也是有关联的,比如 ANALYSIS 对象里面有个 projectId, 不同的节点在新建 PROJECT 的时候使用的是本地 Oracle 的 Sequence,所以 ID 值都不会相同,如果请求方直接把 projectId 发出去,那么消费方通过 projectId 关联对象时,数据就是错乱的。我们根据对业务的分析,发现相关业务表的数据不可能超过一个亿,所以就给每个节点分配一个大数,比如 A 节点是一亿,B 节点是两亿,C 是三亿等等。然后 Spring AOP 监控相关业务表的 INSERT 语句,通过 afterReturning 注解获取 Sequence 的 ID,加上给定的大数作为新的 ID,这样就能保证如 projectId 在各个节点的唯一性。
PS:
为了业务上的需要,我们还引入了一个中心节点,请求方的消息只是点对点发给中心节点,中心节点再转发到所有被邀请节点(中心节点也可以是被邀请节点)。
1. 首先对需要同步的接口进行 AOP 拦截
2. 构建消息,存入本地库,然后发送并等待反馈:
3. 接收方处理接收到的消息
4. 判断是否可以消费接收到的消息
最后,是保证各节点 ID 唯一性的实现: