轻量的流程模块设计

背景

在风控运营工作中存在大量的业务配置,这些业务配置的日常管理工作往往不仅限于新增、编辑,可能还存在一级审批、二级审批、发布到体验环境、发布到正式环境、同步到其他后台系统等等步骤,此外,针对某些可能出现异常或失败的步骤,需要设定重入机制、状态流转。下面以微信钱包错误码为例,分析其业务步骤如下面的表格所示:

步骤序号 步骤操作 描述 状态机描述
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协议的业务流程图如下:
drawing

可以看见流程中的流向(图例为箭头)是非常明确的,个别流向之间设置了网关(图例为菱形),任务方面也包含了用户任务、服务任务。显然,网关越多,流程则越复杂。

对于风控配置运营管理,其流程会简单些,大部分是条带状,任务之间的流向单一(没有过多的条件分支),而且大多是人工操作的任务,其流程大体如下:
drawing

其任务节点的状态机大体如下:
drawing

关于是否需要引入网关的概念,考虑到其功能在于多个流向之间的排他、并行、包容关系,而实际上流向都是直达下一个任务节点的,因此,这里可以免去网关设计,大大简化了方案复杂度。

备注:若一定要做网关实现,那么需要通过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='元数据任务执行状态记录表';

代码设计

前面提到配置管理流程大体如下:
drawing

实际上,在每个任务节点都有些事前、事后动作,包括校验参数、更新任务节点等等,如下图蓝色框所示:
drawing

这种流程逻辑特点符合面向切面特性,可以参考AOP (Aspect Oriented Programming) 的解决方案。AOP的本质是在一系列纵向的控制流程中,把那些相同的子流程提取成一个横向的面,即切面。本处采用回调模式+模板模式+装饰模式做实现,能够实现切面逻辑,同时要尽可能约束实现代码,使任意参与项目的开发者都能编写出差不多的代码风格,保证代码可读性。

其相关的代理类、封箱方法、回调类如下面红框所示(备注:相关代码技术栈基于Java、Spring):
drawing

相关类介绍:

类名 功能描述 技术点 备注
AbstractTodoServiceCronJob 补偿场景、自动任务场景的定时任务 抽象父类,需要根据业务写继承子类
MetadataTaskStatusManager 业务(元数据Metadata)流程管理 这里把业务定义为 "元数据"
TaskFlowManager 流程配置管理
ITaskTemplate 任务节点执行模板,可以进一步约束、规范业务代码写法 参考回调模式、模板模式 回调模式多以Callback为后缀命名
TaskCaller 调用入口,整合事前、事中(主逻辑execute)、事后动作 参考回调模式、装饰模式 装饰模式和代理模式有一定的共通性

流程配置类(TaskFlowManager.java)方法设计:
drawing

上述的类,直接使用在具体业务方法中,这里以【审批】服务接口为例,代码如下:
drawing

该代码方案可以实现具体业务逻辑和流程状态机解耦,整体上清晰明了

成果与展望

关于该模块的迭代发展,有以下几个方向:

  • 对出口端封装统一接口,进一步简化整合的代码,实现开箱可用
  • 完善网关功能,补齐BPMN能力
  • 实现注解方式

最后,关于工作流引擎方案的讨论,例如是应该缩减体积做到组件化,还是说要实现分布式任务调度,还是应该扩大规模实现分布式任务编排呢?目前看来没有确切的指导方案,但行业整体上较倾向于后两者。当然,不同的业务场景有着不一样的诉求,这里也期待该方面能够持续发展

posted @ 2023-05-05 19:34  鱼007  阅读(75)  评论(0编辑  收藏  举报