概述
在日常的业务应用开发中,经常会遇到需要在不同的业务系统中,传输业务信息内容和状态的需求。但在很多场景中,并不是简单的数据交换或者同步,而是根据业务的要求,在业务数据集中,选择一定满足条件的数据,进行简单处理和转换后,再传输到另一个采集系统当中。这时就会进一步提出来,采集系统可能需要支持多个同构的下级业务系统,业务数据可能会不定期的产出,不希望进行重复的处理等等,但是可以支持业务数据的更新。
实现机制
笔者先从逻辑和概念上,来总结一下理想中这一需求实现的过程和机制。
1 下级业务系统开展各种应用,形成各种业务信息条目。在经过了一定的业务流程之后,信息条目达到某些业务状态,并且满足了一些上报的条件,可以作为可以作为向上级系统汇报的信息.
2 信息上报程序模块,作为一个独立于业务流程甚至系统的程序(可以外部部署),定期检查业务信息的状态。在查询到需要上报的信息条目时,构建上报的信息,并向上级系统提供的信息接口请求这些信息。这一过程,可以通过HTTP请求、RPC调用或者消息发送来实现。
3 上级系统的信息接口,接收到信息上报请求,按照业务规则接受这些请求,处理后,响应信息上报的处理结果,如成功或者失败信息等等。
4 上报程序根据响应的结果,修改信息条目的上报状态为"已上报"(成功或者失败)。这个状态,将会作为待上报信息查询的过滤条件。
5 上报数据的准备,是基于数据状态,并且可以分批的。一次只处理有限数量的数据,处理完成,更新数据状态;下次查询,就是另一批待汇报状态的数据;这样的设计主要是考虑到性能和处理的可恢复性。
至此,一个完整的业务信息生成-上报检查-信息上报-状态修改的的完整的信息上报生命周期就已经完成。
在逻辑和流程上,这是一个比较简单而清晰的框架,但实际应用在系统中,却可能遇到很多的问题,这里简单的总结一下:
- 业务流程和信息上报处理需要尽可能的解耦和避免相互干扰
- 需要支持多种类型的信息上报,但又需要一种统一的处理方式,以便于程序维护和扩展
- 需要考虑这个处理过程的性能对于应用系统总体的影响
- 在业务支持上,可能还需要充分考虑的业务信息重新上报(如修改后再上报)和业务信息删除的情况
根据这些问题和要求,笔者在原有业务系统上,提出了相关的改进措施,下面具体讨论。
现状和问题
现有笔者负责运维的系统,虽然在业务需求上,确实是基本满足了。但在实现细节上,有很多问题和不足,急需改进。
现有的实现方式和问题包括:
-
每个小的业务板块,都有一套信息上报的数据结构,直接在业务数据中,增加了业务上报的状态,并且为一种业务,都使用了不同结构的日志数据表,这个问题的原因可能是在前期规划设计时候,并没有从整体和抽象角度考虑这个需求的共性,而是积累和渐进式的开发和实现。
-
原有的产生需要汇报数据的机制也不一样。有的直接基于汇报状态的查询,有的基于定期的实体化视图生成机制,有的需要基于日志写入触发器,来更新汇报状态。这种多样性的处理模式,给维护、扩展和问题排查造成了很多不便。
-
原有的数据汇报,在数据请求的时候,没有分批和限流的机制。笔者就遇到一次需要汇报数据过大,导致查询失败,程序卡住始终无法完成数据汇报的情况。
所以,新的处理流程和方法的设计,需要考虑优化和解决这些问题。为此,笔者提出并实现了以下技术方案。
规划和实现
汇报日志信息表
首先是需要统一抽象和管理各类业务数据汇报的状态和信息。为此,笔者构想并设计了一个汇报日志信息表rp_logs。这个表中包括了以下内容:
- 唯一的业务信息标识 rpid
针对不同的业务对象,通过组合业务代码和业务对象的ID,我们可以设计在系统内部唯一的业务信息标识。需要检查和确保,每项业务,在业务范围内为业务记录提供唯一的标识。
- 汇报状态 status
在逻辑上的汇报状态包括 0-初始状态 0x1000成功和各种可能的失败项目,可以使用编码表示,由于现有的实现方案是同步的,所以在信息同步操作的时候,就可以立刻获得服务端的响应,然后直接修改当前的汇报状态。
- 失败信息 info
某些业务汇报失败时,其实是有详细的描述信息的,可以在汇报日志信息表中,记录这些失败信息,有利于后续问题发现和分析。一般情况下,操作成功是无需额外信息的。
- 操作时间 rptime
这一字段主要用于记录业务汇报操作实际发生的时间,便于分析汇报操作的规模和性能,并且有利于跟踪和查找问题。
这个表,其实就是统一数据汇报状态和操作管理的核心:
- 信息汇报状态,和具体的业务类型和内容无关
- 可以通过数据标识的设计,将业务数据标识和汇报信息关联起来
- 这样,就可以保证通用性和可扩展性(如新的业务类型,只需要添加业务类型标识前缀即可)
业务汇报状态标记
业务汇报状态标记,是在各个业务信息表中,针对每一个数据记录,都使用一个状态标记,来表示当前业务信息的汇报状态。其实这个标记并没有绝对的必要性。因为在逻辑上可以通过关联查询汇报日志信息表,来获得其汇报状态,所以这里有两个技术选择:
- 保留业务数据表中,表示汇报状态的字段
这个方案的优势主要是性能,在数据汇报操作中,无需进行日志表的关联,直接基于状态对数据进行查询。但这个处理虽然比较方便于汇报状态的查询,但却带来了一个问题就是这个状态标记的维护,需要在上报操作后进行维护。现有的方式是在每批次上报后,立刻根据日志的结果,批量更新关联的业务条目汇报状态标记。
- 业务数据表和汇报日志完全结构,在业务数据中没有汇报状态
这个技术处理的好处是结构更加清晰简单。可能的问题是不能以业务状态进行直接的查询,性能可能会稍差一点。笔者根据实际业务使用场景,认为在小批量查询的状态下,这个影响应当不大,是优先选择的技术方案。
主要流程
以某类业务信息为例,数据汇报操作的具体流程如下:
-
通过相关条件和汇报状态(如无汇报记录),查询限定数量的可汇报业务信息
-
构造业务汇报请求,向上级系统提交
-
根据上级系统的响应,来更新汇报信息日志表
-
如果是成功的条目,插入(或者更新)成功状态和操作时间
-
如果是失败的条目,按照失败信息和类型,插入(或更新)失败的状态、信息
-
根据日志信息表的状态,更新业务信息表中,汇报状态的字段的值(可选)
-
执行下一轮查询和请求,直到所有需要处理的数据都完成处理
关于业务汇报信息的请求和响应的实现,其实是另外一个技术问题。如可以使用HTTP接口实现,也可以使用消息队列发送的方式,这和本文探讨的主题也是解耦的,没有直接的关系。唯一需要注意的问题是我们希望能够尽量实现类似"实时"响应的效果,可以让系统及时的更新记录的状态,来保证每次请求时,是新的需要处理的数据。
如果相对而言这个过程的异步效应比较强的话,也可以考虑在完全收到对应的请求结果后,再进行下一轮请求;或者,也可以考虑在请求后,就以一种中间的"处理中"作为状态信息插入日志信息表,然后在异步响应之后,再更新为响应的状态。
参考实现(SQL)
下面是一些参考实现的SQL代码,方便读者理解和移植:
sql
// 汇报日志表
CREATE TABLE rp_logs (
rpid varchar(32) primary key,
status int2 DEFAULT 0 NULL,
rptime timestamptz(6) DEFAULT now() NULL,
info text NULL
);
// 查询和视图
create view vw_report as
select 'E-' || "EventID" rid,
jsonb_build_object('n',"EventName",'t',"EventTime") rcontent from "Events" R
where not exists (select 1 from rp_logs where rpid = 'E-' || "EventID")
order by "CreateTime" desc
limit 50;
//日志和状态更新
insert into rp_logs( rpid, status,info )
values ('E-0002', 200, null)
on conflict(rpid) do update
set (status,info,rptime) = (excluded.status, excluded.info, now())
returning * ;
// 视图执行计划
Type: Limit; ; Cost: 76186.30 - 76192.13
Type: Gather Merge; ; Cost: 76186.30 - 115443.40
Type: Sort; ; Cost: 75186.27 - 75606.86
Type: Hash Join (Anti); ; Cost: 21.93 - 69597.69
Type: Parallel Seq Scan; Rel: Event ; Cost: 0.00 - 64444.66
Type: Hash; ; Cost: 15.30 - 15.30
Type: Seq Scan; Rel: rp_logs ; Cost: 0.00 - 15.30
这里有几点考虑和设计:
- 使用业务类型+业务ID的形式,将汇报日志ID归一化处理,所以这里需要保证业务ID也是唯一的
- 将业务相关数据,统一封装成为JSON字段,这样可以在查询后统一处理
- 单次查询需要设置限制
- 为了方便在代码中编写,将查询简化成为视图使用
- 这里使用 not exists作为查询条件,也可以使用left join null的方式,笔者简单测试了一下,并没有显著差异
- 插入汇报日志,可以重复操作,增加或者更新
汇报状态查询
如果同步成功或者失败,需要在业务系统中进行查询。有两种常用的查询方式,一种是基于业务数据,查询业务信息关联的汇报状态;一种是对于汇报异常的信息,查询关联的业务信息。这两种方式都比较好处理,就是根据类型和ID进行关联即可:
js
// 业务数据汇报状态
select "RecordID" rid, L.status, L.info, L.rptime from "Record_D_Event" R
join rp_logs L on rpid = 'R-' || "RecordID"
where "RecordID" = 1000;
// 异常汇报关联业务数据
select "RecordID" rid, L.status, L.info, L.rptime from "Record_D_Event" R
join rp_logs L on rpid = 'R-' || "RecordID"
where L.status = 500 ;
小结
本文针对笔者在工作中,遇到的一个数据汇报机制实现的场景和问题,提出了改进的技术方案,并且总结出类似业务场景中,一种相对通用的技术方案和流程,和原实现相比,新的技术方案具有更好的可扩展性,处理的一致性,更好的可维护性,并避免了可能的性能问题。