主要概念
BPM
Business Process Management,业务流程管理
BPMN
Business Process Modeling Notation,BPMN是一个广泛接受与支持的,展现流程的注记方法:OMG BPMN标准,BPMN2.0正式版本于2011年1月3日发布,常见的工作流引擎如:Activiti、Flowable、jBPM 都基于 BPMN 2.0 标准。
事件
事件(event)通常用于为流程生命周期中发生的事情建模。事件总是图形化为圆圈。例如:
顺序流
顺序流(sequence flow)是流程中两个元素间的连接器。在流程执行过程中,一个元素被访问后,会沿着其所有出口顺序流继续执行。用从源元素指向目标元素的箭头表示。例如:
网关
网关(gateway)用于控制执行的流向(或者按BPMN 2.0的用词:执行的 “标志(token)” )。网关用其中带有图标的菱形表示。例如:
活动
活动(Activity)用于表示需要执行的行为,任务用带有图标的圆角矩形表示。例如:
CMMN
Case Management Model and Notation,CMMN具有与BPMN不同的基本范例,CMMN没有顺序的流程,它以某种状态对案例建模。
DMN
Decision Model and Notation,DMN的目的是提供一个模型决策结构,从而使组织的策略可以用图形清晰的地描绘出来,这个标准可以用于实现规则引擎。
概览
Flowable是由Activiti的原核心开发人员,基于Activiti5开发的。
Flowable提供了多层次的解决方案
Web App
通过直接搭建Web应用,可直接获得产品化的解决方案。
Flowable IDM: 身份管理应用。为所有Flowable UI应用提供单点登录认证功能,并且为拥有IDM管理员权限的用户提供了管理用户、组与权限的功能。
Flowable Modeler: 让具有建模权限的用户可以创建流程模型、表单、选择表与应用定义。
Flowable Task: 运行时任务应用。提供了启动流程实例、编辑任务表单、完成任务,以及查询流程实例与任务的功能。
Flowable Admin: 管理应用。让具有管理员权限的用户可以查询BPMN、DMN、Form及Content引擎,并提供了许多选项用于修改流程实例、任务、作业等。
REST API
通过搭建REST服务,上层系统可通过调用HTTP API,使用引擎能力。
Flowable BPMN User Guide - REST API
Java API
Flowable提供了一套完整的API,供使用方调用,所有的服务都是无状态的,意味着依赖这套API的服务天然可在分布式环境进行部署。通过直接依赖jar包进行集成,并在各层都留出了接口,使用时可自行灵活扩展。结合使用场景,建议使用Java API进行开发,本文也将重点介绍基于该方式集成的调研结果。
类名 | 说明 |
---|---|
ProcessEngine | 引擎API的总入口 |
RepositoryService | 部署(deployments) 与 流程定义(process definitions) |
RuntimeService | 启动新流程实例、读取与存储流程变量(process variables) 、查询流程实例与执行(Execution) |
TaskService | 查询任务、分配执行用户(assignee) 、认领任务(claim) 、完成任务(complete) |
HistoryService | 提供查询历史数据的能力 |
IdentityService | 用于管理组与用户,可选(引擎运行时并不做用户校验) |
FormService | 表单相关服务,可选(表单不一定要嵌入流程定义) |
ManagementService | 用于读取数据库表与表原始数据的信息,也提供了对作业(job)的查询与管理操作 |
DynamicBpmnService | 可用于动态修改流程定义中的部分内容,而不需要重新部署 |
典型场景
以如下“请假流程”为例,演示Flowable API使用方式,为减少干扰,此处特意没有与Spring集成,以演示最原生的API调用。
⬇️ 符合BPMN 2.0标准的流程图
⬇️ holiday-request.bpmn20.xml 文件内容,及上图所对应的数据文件
部署流程定义
代码逻辑
数据变更
⬇️ ACT_RE_DEPLOYMENT (Deployment) 对应生成了一条记录,表示本次部署。
⬇️ ACT_GE_BYTEARRAY (EngineResource) 生成了两条记录,对应两个资源文件,BYTES_字段保存了文件的原始内容。
⬇️ ACT_RE_PROCDEF (ProcessDefinition) 生成了三条记录,对应到XML中定义的三个流程。
启动流程实例
代码逻辑
数据变更
运行时相关表(ACT_RU_*):只保留当前运行时数据
⬇️ ACT_RU_ACTINST(ActivityInstance) :“活动”数据,包含了流程经过的所有节点(包括连线在内,可以理解为流程经过的全部XML Element)。
⬇️ ACT_RU_EXECUTION(Execution) :“执行”数据,标示了当前的分支路径,一般是遇到网关或并行流程时,会分裂为多个执行,与分裂前的Execution会存在父子关系,只会保留当前正在进行中的Execution。
Q:为什么初始有两个Execution?
A:Flowable 默认会创建一个最父级Execution对象,用来表示“流程实例(ProcessInstance)”,其他表中的PROC_INST_ID,关联的记录实际也是这条ROOT级别的Execution对象。
Q:多条并行的分支路径时,Execution会如何生成?
A:会拆分为多个Execution,并行网关前的其中一个Execution(最后触发分支的这个)会继续延展到网关后的第一个分支,其他的分支会创建出新的Execution。
⬇️ ACT_RU_TASK(Task) :“任务”数据,即用户的代办事项,只会保留当前未完成的任务。
⬇️ ACT_RU_VARIABLE(VariableInstance) :“变量”数据,当前流程实例所设置的变量。
也可以通过setVariableLocal方法将变量绑定在Execution/Task上,作用域相应降低,Local的变量会在流程离开当前作用域时被删除。
Q:如果流程运行到某个表达式,但表达式依赖的变量没有设置会怎么样?
A:会直接抛异常,同时会回滚本次事务。
⬇️ ACT_RU_IDENTITYLINK(IdentityLink) :“身份关联”数据,标志了用户和组在本次流程中的角色。
历史记录相关表(ACT_HI_*),保留全部历史
⬇️ ACT_HI_ACTINST(HistoricActivityInstance) ,与ACT_RU_ACTINST对应。
⬇️ ACT_HI_PROCINST(HistoricProcessInstance) ,流程实例历史。
Q:为什么这个表没有对应的ACT_RU_*表?
A:这个表的数据实际对应的就是ACT_RU_EXECUTION表中,PARENT_ID为NULL数据,可以理解为一次流程Execution的最父级,即为一个ProcessInstance对象。
⬇️ ACT_HI_TASKINST(HistoricTaskInstance) ,与ACT_RU_TASK对应,区别是任务结束后数据仍会保留。
⬇️ ACT_HI_VARINST(HistoricVariableInstance) ,与ACT_RU_VARIABLE对应。
⬇️ ACT_HI_IDENTITYLINK(HistoricIdentityLink) ,与ACT_RU_IDENTITYLINK对应。
当前位置
完成任务
代码逻辑
数据变更
运行时相关表(ACT_RU_*):只保留当前运行时数据
⬇️ ACT_RU_ACTINST(ActivityInstance)
⬇️ ACT_RU_EXECUTION(Execution)
⬇️ ACT_RU_TASK(Task)
⬇️ ACT_RU_VARIABLE(VariableInstance)
⬇️ ACT_RU_IDENTITYLINK(IdentityLink)
历史记录相关表(ACT_HI_*),保留全部历史
⬇️ ACT_HI_ACTINST(HistoricActivityInstance)
⬇️ ACT_HI_PROCINST(HistoricProcessInstance)
无变更
⬇️ ACT_HI_TASKINST(HistoricTaskInstance)
⬇️ ACT_HI_VARINST(HistoricVariableInstance)
⬇️ ACT_HI_IDENTITYLINK(HistoricIdentityLink)
当前位置
结束流程
代码逻辑
数据流转
运行时相关表(ACT_RU_*):只保留当前运行时数据
- 本次流程实例相关的数据均被清空
历史记录相关表(ACT_HI_*),保留全部历史
⬇️ ACT_HI_ACTINST(HistoricActivityInstance)
⬇️ ACT_HI_PROCINST(HistoricProcessInstance)
⬇️ ACT_HI_TASKINST(HistoricTaskInstance)
⬇️ ACT_HI_VARINST(HistoricVariableInstance)
- 无变化
⬇️ ACT_HI_IDENTITYLINK(HistoricIdentityLink)
- 无变化
源码分析
调用层次
- Flowable将所有执行逻辑抽象为“命令”(Command),并通过CommandExecutor对Command对执行调用,CommandExecutor通过职责链模式,对Command包装了多层拦截器,默认拦截器的功能包含了:构建本次命令执行的上下文CommandContext、限定事务边界、打印日志等。
- 通过依赖关系,CommandContext -> (engineConfigurations, sessionFactories),Command中的逻辑可以间接获取到下层的Manager对象以及DBSession对象,进行相应逻辑的实现。
流转逻辑
- Flowable将流程源文件的内容,统一解析为Process对象,将各种节点抽象为FlowElment,并缓存在进程中,通过在Process对象上进行查找,来确认流程的当前节点的类型,以及后续节点的类型,这套逻辑都在内存中完成,理论上会比较高效(见左图)。
- Flowable实现了一套类似深度优先的机制,将涉及到流程流转的逻辑,统一抽象为“操作”AbstractOperation,CommandInvoker通过对操作队列的顺序执行,完成流程节点的流转,直至当前的各个Execution都达到“等待状态”(见右图)。
仍然以以下流程为例,本次换成对请假申请进行拒绝。
具体流转步骤如下
编号 | 当前操作 | 节点流转 | 数据变更 |
---|---|---|---|
1 | TriggerExecutionOperation | UserTask | 无 |
2 | TakeOutgoingSequenceFlowsOperation | UserTask -> SequenceFlow | ActivityInstance:UserTask 标记为结束Execution:变更当前节点为 SequenceFlow |
3 | ContinueProcessOperation | SequenceFlow -> ExcusiveGateway ->SequenceFlow | ActivityInstance:创建SequenceFlow并标记为结束Execution:变更当前节点为ExcusiveGatewayActivityInstance:创建ExcusiveGateway并标记为开始Execution:变更当前节点为SequenceFlow |
4 | TakeOutgoingSequenceFlowsOperation | SequenceFlow | ActivityInstance:标记ExcusiveGateway为结束 |
5 | ContinueProcessOperation | SequenceFlow -> ServiceTask | ActivityInstance:创建SequenceFlow并标记为结束Execution:变更当前节点为ServiceTaskActivityInstance:创建ServiceTask并标记为开始。 |
6 | TakeOutgoingSequenceFlowsOperation | ServiceTask -> SequenceFlow | ActivityInstance:标记ServiceTask记录为结束Execution:变更当前节点为SequenceFlow |
7 | ContinueProcessOperation | SequenceFlow -> EndEvent | ActivityInstance:创建SequenceFlow并标记为结束Execution:变更当前节点为EndEventActivityInstance:创建EndEvent并标记为开始 |
8 | TakeOutgoingSequenceFlowsOperation | EndEvent | ActivityInstance:标记EndEvent记录为结束 |
9 | EndExecutionOperation | 删除本次运行时的相关数据 |
注:该例子流程比较简单,如果存在多个并行分支,Execution则会分裂出多个,通过这套机制,可以比较好的降低复杂性。
异步执行
Flowable 实现了一套异步执行器,用于的将一些逻辑切换为异步进行执行。
定时器(Timers)
保存在ACT_RU_TIMER_JOB表中,并带有给定的到期日期。异步执行器中有一个线程,周期性地检查是否有需要触发的定时器。当需要触发定时器时,从JOB表中移除该定时器,并创建一个异步作业(async job)。
异步延续 (Asynchronous Continuations)
Flowable 支持将某些任务节点配置为异步执行,只需要按以下方式配置节点属性,即可使任务的执行切换为异步执行。Flowable默认通过DB实现了一个类似消息队列的机制,引擎通过启动线程池来完成消息的消费,Flowable也留出了扩展性,可以替换为使用真实的消息队列实现异步。
<serviceTask id="service1" name="Generate Invoice"
flowable:class="my.custom.Delegate"
flowable:async="true" />
复制代码
异步历史(Asynchronous History)
Flowable 支持将History数据的写入配置为异步,只需要配置
isAsyncHistoryEnabled=true
,即可将历史数据的写入切换为异步写入,缩短客户端的等待时间。附:flowable异步历史性能基准测试
事件机制
- Flowable实现了一套标准的事件机制,并在支持对多种事件进行监听,当Listener执行失败时也支持事务的回滚(可选),详见:3.18. 事件处理器
- 通过这套机制,可以在流程进行的同时,通过监听相应的时间,对业务系统进行额外操作。
变量作用域
引擎可在多个级别设置变量,作用域各不相同,设置方法如下:
RuntimeService
void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
TaskService
void setVariable(String taskId, String variableName, Object value);
void setVariableLocal(String taskId, String variableName, Object value);
VariableScope
void setTransientVariable(String variableName, Object variableValue);
void setTransientVariableLocal(String variableName, Object variableValue);
// setVariables,setVariableLocals 为对应函数的批量形式,以上省略
复制代码
作用域如下
方法 | 说明 |
---|---|
RuntimeService.setVariable | 变量关联最*Execution,当前流程内的Execution/Task 均可以获取到这些变量。 |
RuntimeService.setVariableLocal | 变量关联指定的ExecutionId,只有对应的Execution(包括子执行)及所属的Task可以获取到这些变量。 |
TaskService.setVariable | 同 RuntimeService.setVariable。 |
TaskService.setVariableLocal | 变量关联指定的TaskId,只有对应的Task可以获取到这些变量。 |
VariableScope.setTransientVariable | 不持久化,仅存在于内存中,会在下一个“等待状态”消失,等待状态意味着流程实例会持久化至数据存储中。等待状态可以是用户任务/异步活动。 |
VariableScope.setTransientVariableLocal | 不持久化,变量作用域取决于是对哪个对想设置。 |
以下代码用于验证
代码输出结果
Execution 15004 variables:
var_local_execution_15004: 1
var_local_execution_15001: 1
var_execution_15004: 1
var_execution_15001: 1
var_task_15008: 1
Execution 15001 variables:
var_local_execution_15001: 1
var_execution_15004: 1
var_execution_15001: 1
var_task_15008: 1
Task 15008 variables:
var_local_execution_15004: 1
var_local_execution_15001: 1
var_execution_15004: 1
var_local_task_15008: 1
var_execution_15001: 1
var_task_15008: 1
复制代码
持久化数据
扩展性
-
Flowable 在各个层次均留出了接口,用于扩展,通过使用ProcessEngineConfiguration进行配置,可以轻松替换各种实现,如:
- 可通过替换 DeploymentDataManager 接口的实现,将底层存储替换为其他任意存储介质。
- 可通过替换 IdGenerator 接口的实现,将ID生成器替换为其他类型。
-
Flowable 各模块解耦比较充分,引擎核心并不依赖一些上层应用,比如身份相关模块、表单相关模块,可以只使用引擎做状态流转,但用户身份等仍然使用公司的主数据。
性能
缓存机制
通过对源码的分析,Flowable中的缓存大致分为两类:
- 对流程定义(ProcessDefinition)这类数据的缓存,因为变更较少且访问频繁,将数据解析后常驻缓存在了进程中,且因每次部署时都是重新插入新的数据,所以不会存在有一致性的问题。
- 对于各数据实体的缓存,Flowable 设计了生命周期为一次命令的缓存,这类缓存能有效降低一次调用中相同数据对DB的多次查询,并随着CommandContext的销毁而销毁。
同时,Flowable留出了接口,可以比较轻松的进行扩展,将缓存替换成Redis这类集中式缓存。
长流程
因为流程只能单向流动,Flowable的机制保证了,即使流程中节点很多,也只需要加载当前节点附近的节点,并不会因为处在一个长流程中,而使每一步的耗时线性增加。
吞吐量
Flowable没有一些NIO、Reative相关的实现,所以吞吐量取决于一次请求中的IO耗时于线程数配置的。
多租户
原生支持
Flowable原生支持多租户,如“典型场景”中的使用方式,只需要在调用API时传入tenantId,即支持在某个租户下进行操作,体现在数据上,便是各实体均有TENANT_ID字段用于标志租户。
数据隔离
Flowable默认将所有租户都放在相同的表中,用TENANT_ID区分,进行数据隔离。但由于Flowable的高度可定制,可以通过重载配置类,将数据分配到不同database,进行保存。官方也提供了对应的配置类实现:MultiSchemaMultiTenantProcessEngineConfiguration。
数据迁移
当某个租户需要从集群迁移出来,进行独立集群部署/私有化部署时,可通过数据库表的TENANT_ID导出对应数据,进行相应部署。这部分目前没有发现官方有现成的迁移工具,但理论上复杂性并不高,主要需要关注如何平滑迁移集群流量这类常见问题。
官方同样提供了changeDeploymentTenantId这种一键替换某个租户的TENANT_ID的方法,这个在数据量大的情况下可能会导致事务很大,个人建议这类操作还是自行控制更合适。
相关竞品
Activiti
目前的最新版 7.1.x,从2016年与Flowable分家之后,主要方向在云化;而Flowable主要专注于升级引擎的核心,Activiti 与 Flowable 个人更推荐后者。
Camunda
相关的资料比较少,是三个中最早支持CMMN、DMN的引擎,功能上的对比可以参考本文中的对比表格:工作流选型专项,Camunda or flowable or?(未完全求证),Camunda是否比Flowable更合适还有待调研。
作者:孔咯
链接:https://juejin.cn/post/7002503394707374111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。