新流程引擎设计文档

新流程引擎设计文档

新流程引擎开发的目的是为了解决在上海银行渠道中台实施过程中发现的性能问题。原有的流程引擎采用activiti作为流程引擎的核心,在当前的使用场景中并不匹配,大量的SQL操作以及冗余的功能设计导致引擎耗时过长。

因此,新方案将从以下几个层面重新设计短流程工作流:

  1. 兼容BPMN模型。基于flowable解析bpmn文件获取模型。
  2. 按需实现功能。不再使用全功能的activiti,改为参考flowable的process和agenda的设计并重新实现,保留扩展性的同时根据实际需求实现功能。
  3. 重新设计数据库表结构。只保留流程实例表,任务及参数均保存在流程实例中,只需要一次获取、一次更新操作即可完成流程创建、节点提交、节点回滚等操作。
  4. 持久化解耦。SDK上层实现逻辑与持久化实现解耦,方便后续对表结构进行修改优化。
  5. 灵活的持久化策略。新设计通过持久化解耦的方式支持多种持久化方案。1. 方法调用结束后马上持久化 2. 方法调用结束后缓存到内存,由调用方自行决定持久化时机
  6. 乐观锁。使用版本字段对流程实例、任务、参数等进行控制(乐观锁),代替开启强事务的方式。
  7. 本地化。流程定义文件不再入库,采用本地文件方式读取并进行内存缓存(预留扩展点,默认实现本地文件读取)。

详细设计

将通过分解说明的方式概述各关键点的设计细节。

1. 兼容BPMN模型

现有的方案通过BPMN规范定义流程,新方法也采用同样的规范以保持最大的兼容性,因此在开发模式上不需要进行改动。

新方案依赖flowable实现对BPMN文件的解析,避免重新实现带来额外的风险。

2. 按需实现功能

综合需求进行分析,新方案将实现启动流程实例 提交对象 回滚对象 获取对象规则 这几个能力(注:对象即BPMN定义中的用户任务)。

2.1 启动流程实例

主要流程:

  1. 新建流程实例时,通过id生成策略生成唯一id
  2. 最多只需要一次保存动作即可
  -- 插入流程实例
      insert into bizflow_procinst (id_,name_,business_key_,proc_def_id_,start_time_,rev_,executions_,tasks_,status_)
        values ('id_','name_','business_key_','proc_def_id_','2024-07-26 00:00:00',0,'<执行分支数据>','<任务数据>',0);

2.2 提交对象

主要流程:

  1. 通过一次仓储操作获取到流程实例对象后,在内存中完成提交任务以及计算下一个节点的操作,最后一次更新流程实例的仓储操作。
  2. 并发提交失败,则自旋重新合并数据,重新计算任务节点并提交。
  3. 流程实例完成后通过标记的方式完成软删除(status=1)。
  -- 查询流程实例
      select id_,name_,business_key_,proc_def_id_,start_time_,rev_,executions_,tasks_,status_ from bizflow_procinst where id_='id_';
  -- 更新流程实例(流程实例未完成时)
      update bizflow_procinst set rev_=<当前版本>+1,executions_='<执行分支数据>',tasks_='<任务数据>' where id='id_' and rev_=<当前版本>;
  -- 删除流程实例(流程实例完成时软删除)
      update bizflow_procinst set rev_=<当前版本>+1,executions_='<执行分支数据>',tasks_='<任务数据>',status_=1 where id='id_' and rev_=<当前版本>;

2.3 回滚对象

主要流程:

  1. 通过一次仓储操作获取到流程实例对象后,在内存中完成对象回滚操作,最后一次更新流程实例的仓储操作
  2. 如果过程中出现并发回滚导致冲突,则回滚失败(回滚只能有一个生效)。
  -- 查询流程实例
      select id_,name_,business_key_,proc_def_id_,start_time_,rev_,executions_,tasks_,status_  from bizflow_procinst where id_='id_';
  -- 更新流程实例
      update bizflow_procinst set rev_=<当前版本>+1,executions_='<执行分支数据>',tasks_='<任务数据>' where id='id_' and rev_=<当前版本>;

2.4 获取对象规则

主要流程:

  1. 获取缓存中的流程定义模型后,从本地获取对应的规则文件并解析成模型对象(初次获取后缓存),然后在内存中完成对象规则的解析操作。
  2. 获取流程实例是因为需要根据流程实例的变量进行规则匹配。
  -- 查询流程实例
      select id_,name_,business_key_,proc_def_id_,start_time_,rev_,executions_,tasks_,status  from bizflow_procinst where id_='id_';

2.5 持久化流程实例

主要流程:

  1. 由应用层触发流程实例的持久化,引擎检查当前的持久化实现方式确定是否需要持久化。
  2. 已完成且未持久化过的流程实例,即从创建到完成都在一次请求中完成的流程实例,不需要实际的保存动作。

3. 重新设计数据库表结构

4. 持久化解耦

数据库操作、本地文件操作等外部资源的操作,通过repository防腐接口进行解耦。

public interface IBpmnModelRepository extends IRepository<ProcessDefinition> {  
  
    ProcessDefinition findById(String processDefinitionId) throws NoSuchDefinitionException;  
}
public interface IProcessInstanceRepository extends IRepository<ProcessInstance> {  
    ProcessInstance findById(String id);  
    boolean save(ProcessInstance processInstance);  
    boolean deleteById(String id);  
}

5. 乐观锁

activiti现有的机制采用了事务锁的方式保证一致性,在新方案中,基于以下特征,可以改为基于version机制的一致性。

采用乐观锁机制后,如果出现版本冲突导致的操作失败,SDK层面会通过自旋重试的方式重新合并数据并提交。

6. 本地化

activiti的流程定义文件会进行入库存储,新方案则默认采用本地目录存放的方式直接读取。

  1. 保留扩展性,新方案通过repository接口解耦具体的实现,方便后续变更为其它实现方式。
  2. 文件读取后会缓存到内存中,避免重复的io操作。
  3. 后台线程通过时间戳对比的方式实现文件更新后重新加载。

7. 唯一序列号

唯一序列号主要用于流程实例ID、任务ID、流程分支ID等需要唯一ID的场景。由于需求量较大,为了避免频繁申请ID导致的性能瓶颈,在唯一序列号的设计上,提供了两个申请ID的方法:

public interface IdGenerator {  
    String nextId();  
  
    String nextId(String baseId);  
}
  1. 第一个无参的申请方法,用于申请流程实例ID
  2. 第二个带baseId参数的申请方法,用于申请任务ID、流程分支ID等。在baseId具备唯一性的基础上,可以快速通过其它方式生成新的ID(如UUID)

默认实现为基于数据库的号段模式生成唯一序列号,同时为了减少序列号耗尽重新申请号段导致的额外耗时,提供预申请号段的方式,通过后台线程提前申请部分号段存放在内存中备用。

基于数据库的号段模式

特点:局部有序,预分配,自旋更新,自定义号段大小,多缓冲设计

该模式依赖数据库实现号段分配,更新时采用version版本号控制加上自旋更新的方式避免开启事务锁,同时允许应用自定义申请的号段大小(可以根据业务量评估)。为了尽可能避免号段耗尽时重新申请号段导致应用耗时出现明显的波动,加入了预分配号段以及多缓冲的机制支持,可以按需选择。

  1. 预分配号段:通过后台线程提前申请部分号段存放在内存中备用。预分配号段的数量与缓冲区的数量相同。相对的,这种方式也会导致更多的号段资源浪费。
  2. 多缓冲。每个缓冲区都申请同样大小的号段,优先从第一个缓冲区获取id,如果获取失败则获取下一个缓冲区的id,知道获取到可用的id或者全部缓冲区都耗尽则随机选择一个缓冲区同步等待更新号段。这种方式目的在于提供更多可用id空间的同时尽可能避免号段资源的浪费,但相对的也会导致更频繁的号段申请操作,通常建议配合预分配号段功能使用。

序列号获取

预分配号段