轻量的流程模块设计
背景
在风控运营工作中存在大量的业务配置,这些业务配置的日常管理工作往往不仅限于新增、编辑,可能还存在一级审批、二级审批、发布到体验环境、发布到正式环境、同步到其他后台系统等等步骤,此外,针对某些可能出现异常或失败的步骤,需要设定重入机制、状态流转。下面以微信钱包错误码为例,分析其业务步骤如下面的表格所示:
步骤序号 | 步骤操作 | 描述 | 状态机描述 |
---|---|---|---|
1 | 编辑 | 新增、修改 | 提交即流转下一步 |
2 | 审核(生成体验环境配置) | 生成体验环境配置,提供给策略后台系统使用 | 审批通过则流转下一步 |
3 | 确认发布到体验环境 | 确认策略后台系统已发布到体验环境 | 点击确认即流转下一步 |
4 | 体验环境验收 | 体验环境做验收测试 | 点击通过即流转下一步 |
5 | 审批(生成正式环境配置) | 生成正式环境配置,提供给策略后台系统使用 | 审批通过则流转下一步 |
6 | 同步到第三方系统 | 主动同步到其他后台系统 | 执行时标记为执行中;执行成功即流转下一步;执行失败则重试 |
7 | 确认发布到正式环境 | 确认策略后台系统已发布到正式环境 | 点击确认即终态(走完全部流程) |
从表中可见,走完一套编辑上线流程,要经历7个步骤操作,每个步骤的操作都要定义执行中、执行成功、执行失败状态,最终的状态枚举个数是 7*3=21个。
风控运营工作中存在不少类似的较为笨重的配置,其典型的特点是步骤节点多,且每个步骤都要考虑多种状态,整体复杂度不亚于上述的微信钱包错误码场景。如果都通过一连串的if-else实现,那么最终会产生一大段代码,由不同的程序员实现可能会产生眼花缭乱的效果,可预见有着极高耦合度、代码冗长、可读性差等不良效果,不利于后续的维护。
对于这种多步骤多状态的场景,使用工作流引擎则非常合适,除了能够解耦步骤节点之间的联系,还能实现复杂的流转机制,能够做到业务逻辑和状态机解耦分离,代码方面表现非常优雅。
技术选型
目前比较受欢迎的开源工作流引擎有Activiti、Flowable,两者都能满足BPMN2.0协议特性,功能也非常完善,涵盖了BPMN协议全部功能的实现、协议解析、流程绘制等。相对地,其体积也较大,涵盖几十个表,若要实现一个完整的、独立的流程编排系统,需要投入一定的研发时间。
对于自研工作流引擎的实现,其目标定是建立一个松解耦的、轻量的组件,可以保证快速实现状态机、流程编排功能。
技术实现
建模设计
关于流程编排,BPMN2.0协议就有一定的参考意义。BPMN定义了以下几种基本对象:
对象 | 描述 | 功能 | 分类 | 图例 |
---|---|---|---|---|
流程(Process) | - | |||
事件(Event) | 开始事件、结束事件、消息开始事件 | 圆形 | ||
网关(Gateway) | 描述多个流向之间的联系、关系、顺序条件 | 用来控制流程的流向(注意不是指任务) | 排他网关(Exclusive Gateway) 、并行网关(Parallel Gateway)、包容网关(Inclusive Gateway) | 菱形 |
流向/顺序流(Flow) | 连接两个流程节点的连线 | 描述任务节点之间的顺序关系 | 箭头 | |
任务(Task) | 是一个流程(Process)中的关键原子级的活动 | 用来指代一个由人或计算设备来完成的活动 | 用户任务(User Task)、服务任务(Service Task) | 矩形 |
以商务申请场景举例,满足BPMN协议的业务流程图如下:
可以看见流程中的流向(图例为箭头)是非常明确的,个别流向之间设置了网关(图例为菱形),任务方面也包含了用户任务、服务任务。显然,网关越多,流程则越复杂。
对于风控配置运营管理,其流程会简单些,大部分是条带状,任务之间的流向单一(没有过多的条件分支),而且大多是人工操作的任务,其流程大体如下:
其任务节点的状态机大体如下:
关于是否需要引入网关的概念,考虑到其功能在于多个流向之间的排他、并行、包容关系,而实际上流向都是直达下一个任务节点的,因此,这里可以免去网关设计,大大简化了方案复杂度。
备注:若一定要做网关实现,那么需要通过MQ、定时任务、时间轮等类似技术来实现事件监听,会增加复杂度、开发时间。
综上所述,从建模方面需要考虑的对象包括:
对象 | 描述 |
---|---|
流程(Process) | |
流向/顺序流(Flow) | 任务之间的流向,需要结合任务状态 |
任务(Task) | 任务节点的业务逻辑,例如编辑提交、审批等等 |
任务状态(task_status) | 记录当前执行任务的状态,一般包含成功、失败,具体枚举值参见下表 |
其中,任务状态的枚举描述如下:
状态描述 | 英文 | 数值 | 适合场景 |
---|---|---|---|
初始化 | INIT | 0 | |
执行中 | PROCESS | 1 | 执行时状态 |
运行时超时或状态未知、待重入 | UNKNOWN | 4 | 异步操作场景、定时任务场景、对接第三方容易发生超时或失败 |
通过 | PASS | 6 | 审批场景、执行任务逻辑成功场景 |
驳回 | REJECT | 7 | 审批场景 |
失败 | FAIL | 9 | 执行任务逻辑失败场景 |
定义枚举类如下:
public class TaskStatus {
// 初始化
public static final String INIT = "0";
// 执行中
public static final String PROCESS = "1";
// 运行时超时、状态未知、待重入
public static final String UNKNOWN = "4";
// 通过
public static final String PASS = "6";
// 驳回、拒绝
public static final String REJECT = "7";
// 执行失败
public static final String FAIL = "9";
}
备注:若任务之间的流向,是有多个的,例如任务执行失败后需要判断重跑次数来决定下一任务:若小于2次则重试,若2到5次之间则告警+重试,若大于5次则失败,那么这时候就需要针对这些失败结果做分流,需要定义更多的失败状态。归根到底,流向(flow)是任务执行状态的延伸、映射。
表设计
注意:元数据意为业务数据,对于本系统即风控业务配置,举例微信钱包错误码,定义其Fmetadata_type="wxErrCodeV1"。
相关DDL如下:
CREATE TABLE `t_process` (
`Fid` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`Fprocess_id` varchar(32) NOT NULL COMMENT '流程标识',
`Fprocess_name` varchar(32) DEFAULT NULL COMMENT '命名',
`Fprocess_desc` varchar(64) DEFAULT NULL COMMENT '描述',
`Fdel_flag` int(2) NOT NULL DEFAULT '0' COMMENT '1-yes,0-no',
`Fcreate_time` datetime NOT NULL,
`Fcreate_user` varchar(32) DEFAULT NULL,
`Fmodify_time` datetime NOT NULL,
`Fmodify_user` varchar(255) DEFAULT NULL,
PRIMARY KEY (`Fid`),
UNIQUE KEY `idx_process_id` (`Fprocess_id`) USING BTREE,
KEY `idx_modify_time` (`Fmodify_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='流程配置表';
CREATE TABLE `t_process_task` (
`Fid` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`Fprocess_id` varchar(32) NOT NULL COMMENT '流程标识',
`Ftask_id` varchar(32) NOT NULL COMMENT '任务标识',
`Ftask_name` varchar(32) DEFAULT NULL COMMENT '命名',
`Ftask_desc` varchar(64) DEFAULT NULL COMMENT '描述',
`Ftask_type` varchar(32) DEFAULT NULL COMMENT '类型',
`Fdel_flag` int(2) NOT NULL DEFAULT '0' COMMENT '1-yes,0-no',
`Fcreate_time` datetime NOT NULL,
`Fcreate_user` varchar(32) DEFAULT NULL,
`Fmodify_time` datetime NOT NULL,
`Fmodify_user` varchar(255) DEFAULT NULL,
PRIMARY KEY (`Fid`),
UNIQUE KEY `idx_process_task` (`Fprocess_id`,`Ftask_id`) USING BTREE,
KEY `idx_modify_time` (`Fmodify_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='流程任务节点配置表';
CREATE TABLE `t_process_task_flow` (
`Fid` int(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
`Fprocess_id` varchar(32) NOT NULL COMMENT '流程标识',
`Fsource_task_id` varchar(32) NOT NULL COMMENT '当前任务标识',
`Fsource_task_status` varchar(16) DEFAULT NULL COMMENT '当前任务执行状态',
`Fnext_task_id` varchar(64) DEFAULT NULL COMMENT '下一任务节点',
`Fdel_flag` int(2) NOT NULL DEFAULT '0' COMMENT '1-yes,0-no',
`Fcreate_time` datetime NOT NULL,
`Fcreate_user` varchar(32) DEFAULT NULL,
`Fmodify_time` datetime NOT NULL,
`Fmodify_user` varchar(255) DEFAULT NULL,
`Fdesc` varchar(32) DEFAULT NULL,
PRIMARY KEY (`Fid`),
UNIQUE KEY `idx_process_task_status` (`Fprocess_id`,`Fsource_task_id`,`Fsource_task_status`) USING BTREE,
KEY `idx_modify_time` (`Fmodify_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='流程任务节点流向关系表';
CREATE TABLE `t_metadata_task_status` (
`Fid` bigint(16) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`Fmetadata_type` varchar(32) NOT NULL COMMENT '元数据类型、业务类型 ',
`Fmetadata_id` varchar(32) NOT NULL COMMENT '元数据主键 ',
`Fprocess_id` varchar(32) NOT NULL COMMENT '流程标识',
`Ftodo_task` varchar(62) NOT NULL COMMENT '当前任务',
`Ftodo_task_status` varchar(8) NOT NULL COMMENT '当前任务执行状态',
`Ftodo_task_type` varchar(32) NOT NULL COMMENT '当前任务类型',
`Fcreate_user` varchar(32) NOT NULL COMMENT '创建数据用户',
`Fmodify_user` varchar(32) DEFAULT NULL COMMENT '更新数据用户',
`Fcreate_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
`Fmodify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`Fid`),
KEY `idx_metadata` (`Fmetadata_type`,`Fmetadata_id`,`Ftodo_task_status`,`Ftodo_task_type`),
KEY `idx_modify_time` (`Fmodify_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='元数据任务执行状态表';
CREATE TABLE `t_metadata_task_status_log` (
`Flog_id` bigint(16) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`Fid` bigint(16) unsigned NOT NULL COMMENT 't_metadata_task_status表主键',
`Fmetadata_type` varchar(32) NOT NULL COMMENT '元数据类型、业务类型 ',
`Fmetadata_id` varchar(32) NOT NULL COMMENT '元数据主键 ',
`Fprocess_id` varchar(32) NOT NULL COMMENT '流程标识',
`Ftodo_task` varchar(62) NOT NULL COMMENT '当前任务',
`Ftodo_task_status` varchar(8) NOT NULL COMMENT '当前任务执行状态',
`Ftodo_task_type` varchar(32) NOT NULL COMMENT '当前任务类型',
`Fcreate_user` varchar(32) NOT NULL COMMENT '创建数据用户',
`Fmodify_user` varchar(32) DEFAULT NULL COMMENT '更新数据用户',
`Fcreate_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
`Fmodify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`Flog_id`),
KEY `idx_metadata` (`Fmetadata_type`,`Fmetadata_id`,`Ftodo_task_status`,`Ftodo_task_type`),
KEY `idx_modify_time` (`Fmodify_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='元数据任务执行状态记录表';
代码设计
前面提到配置管理流程大体如下:
实际上,在每个任务节点都有些事前、事后动作,包括校验参数、更新任务节点等等,如下图蓝色框所示:
这种流程逻辑特点符合面向切面特性,可以参考AOP (Aspect Oriented Programming) 的解决方案。AOP的本质是在一系列纵向的控制流程中,把那些相同的子流程提取成一个横向的面,即切面。本处采用回调模式+模板模式+装饰模式做实现,能够实现切面逻辑,同时要尽可能约束实现代码,使任意参与项目的开发者都能编写出差不多的代码风格,保证代码可读性。
其相关的代理类、封箱方法、回调类如下面红框所示(备注:相关代码技术栈基于Java、Spring):
相关类介绍:
类名 | 功能描述 | 技术点 | 备注 |
---|---|---|---|
AbstractTodoServiceCronJob | 补偿场景、自动任务场景的定时任务 | 抽象父类,需要根据业务写继承子类 | |
MetadataTaskStatusManager | 业务(元数据Metadata)流程管理 | 这里把业务定义为 "元数据" | |
TaskFlowManager | 流程配置管理 | ||
ITaskTemplate | 任务节点执行模板,可以进一步约束、规范业务代码写法 | 参考回调模式、模板模式 | 回调模式多以Callback为后缀命名 |
TaskCaller | 调用入口,整合事前、事中(主逻辑execute)、事后动作 | 参考回调模式、装饰模式 | 装饰模式和代理模式有一定的共通性 |
流程配置类(TaskFlowManager.java)方法设计:
上述的类,直接使用在具体业务方法中,这里以【审批】服务接口为例,代码如下:
该代码方案可以实现具体业务逻辑和流程状态机解耦,整体上清晰明了
成果与展望
关于该模块的迭代发展,有以下几个方向:
- 对出口端封装统一接口,进一步简化整合的代码,实现开箱可用
- 完善网关功能,补齐BPMN能力
- 实现注解方式
最后,关于工作流引擎方案的讨论,例如是应该缩减体积做到组件化,还是说要实现分布式任务调度,还是应该扩大规模实现分布式任务编排呢?目前看来没有确切的指导方案,但行业整体上较倾向于后两者。当然,不同的业务场景有着不一样的诉求,这里也期待该方面能够持续发展