
工作流引擎里若是排一个“让人又爱又恨的节点榜”并行网关大概率能进前三。爱它是因为它能一次把流程拆成多条并发分支几个没有先后依赖的任务同时跑整体耗时从“几个任务之和”变成“最慢的那条分支”恨它是因为一旦有人没搞懂它“汇聚必须等齐所有分支”的语义流程就会突然卡住怎么查都找不到原因。这篇是Activiti 6.0系列里的第六篇前几篇把流程定义、部署、启动、用户任务和排他网关讲完了这次集中把并行网关彻底讲透——从BPMN设计、XML写法、部署启动到并发执行后的表数据变化以及真实项目里最容易踩的坑。如果你正在做审批流、任务编排、多系统联动或者只是想把一个串行流程优化成并发流程这篇文章可以拿来直接参考。1. 为什么需要并行网关先搞清楚它的职责边界1.1 并行网关与排他网关的分工完全不同很多初学者一看到“网关”两个字就觉得所有和分支判断相关的逻辑都该交给网关。实际上BPMN里的网关各管一摊排他网关和并行网关是两种极端相反的控制流语义。排他网关Exclusive Gateway是“多选一”到了这个节点引擎会根据出线上的条件表达式算出一个结果只激活一条分支往前走。比如报销单金额小于5000走普通审批大于5000走老板审批这就是典型的排他。而并行网关Parallel Gateway是“全都要”它不判断任何条件所有出线都会被执行一遍。用一句大白话区分排他网关是岔路口只能挑一条路走并行网关是复制机把流程实例复制成多份同时往前走。这个区别直接决定了建模思路。某公司的差旅报销流程里财务初审、项目经理审批、法务合规确认这三件事之间没有先后依赖但串行跑下来要两三天。后来我把这块改成并行网关拆分三个任务同时落到三个处理人的待办里整体耗时直接从三天压缩到一天。这就是并行网关的核心价值没有依赖关系的环节不该排队而应该并发。1.2 并行网关适合与不适合的场景并行网关适合的场景有三个共同点分支数量固定、分支职责不同、分支之间没有数据依赖。例如“采购审批里技术部门出一份测试报告法务部门出一份合同意见财务部门出一份预算确认”三个角色完全不同数量在设计期是确定的彼此不需要等对方的产出物那就非常适合并行。再比如“代码上线前开发自测、测试回归、安全扫描同时进行”也属于并行分支。不适合的场景同样值得说清楚。第一类分支之间有严格先后依赖的流程比如“必须先做完方案评审才能开始编码”这种场景用并行等于自己给自己挖坑。第二类同一角色有N个人参与、人数动态变化的会签场景不应该用并行网关去画死几个分支而应该用多实例节点multiInstanceLoopCharacteristics让引擎根据集合变量动态展开子任务。第三类需要按条件动态决定“走哪几条分支”的场景并行网关也不合适这类需求属于包含网关Inclusive Gateway的管辖范围。这个边界想明白了后面设计流程才能少走弯路。并行网关不是万能的加速按钮它只是一扇“同时放行多辆车”的大门门后的路怎么走还得看整体流程设计。2. 先在BPMN里看清“拆”与“合”的发生过程2.1 流程结构拆解从单线程变成多线程再回到单线程一个最基础的并行网关流程结构是这样的开始事件 - 并行网关A拆分 - 分支任务1和分支任务2 - 并行网关B汇聚 - 后续任务 - 结束事件。并行网关A做的事情叫“拆分”fork它把进入网关的一个执行实例扩展成多个执行实例每个实例各走一条出线。这个过程不是把任务复制一份而是引擎在ACT_RU_EXECUTION表中创建了多个子执行实例它们共享同一个流程实例ID各自维护自己的执行位置和局部数据。并行网关B做的事情叫“汇聚”join它等待所有进入该网关的分支都到达最后一个分支到达时才触发后续节点。这里有一个很多人容易忽略的设计习惯同一个网关在BPMN语义上既可以当拆分网关也可以当汇聚网关ID相同也能用。但我在实际项目里从来不这么干一定会拆成两个节点分开命名比如parallelGateway1负责拆分、parallelGateway2负责汇聚。原因很简单流程图是给团队看的拆开命名之后任何一个看流程图的人都能一秒判断哪里开始并行、哪里开始合流监控平台上的节点日志也更容易对账。2.2 XML里的定义细节与常见参数用Activiti 6.0定义并行网关非常简洁核心就是一行XML节点。每个并行网关节点必须有唯一的id和name相邻节点之间通过sequenceFlow连接sequenceFlow的sourceRef和targetRef必须和节点id精确匹配。并行分支的数量不是动态算出来的而是由并行网关上挂了多少条出线决定的。两条出线就是两个并行任务要改成三条就再加一条出线和一个目标节点重新部署流程即可。并行网关的出线上不要写条件表达式。这是并行网关和排他网关的一个关键差异。排他网关的出线上可以写诸如${amount 5000}这样的条件引擎会据此选路并行网关的语义是“无条件全走”即便你在出线上写了条件引擎也会把条件忽略掉所有出线照常执行。我在一个项目里见过同事A把金额判断写在并行网关出线上结果两条分支同时被执行业务数据差点出了问题。所以看到菱形中间带加号的图标就不要再给它加判断逻辑了。3. 完整Demo从流程定义到并发执行全流程复现3.1 准备环境与依赖引入开始写代码之前先说清楚运行环境。我下面用的是Activiti 6.0.0原生引擎API不依赖Spring Boot这样版本问题最少。你只需要一个Java 8环境一个Maven项目把下面三个依赖加进pom.xml。dependency groupIdorg.activiti/groupId artifactIdactiviti-engine/artifactId version6.0.0/version /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId version1.4.196/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version1.7.25/version /dependencyH2是内嵌数据库不需要额外安装。Activiti引擎启动时会自动在内存里建好ACT_开头的系列表方便我们做实验。如果你想看数据库里的落盘数据也可以换成MySQL只要把JDBC连接信息改一下就行。不过第一次跑Demo的时候我建议就用H2数据不会污染本地环境随时可以重来。3.2 流程定义文件编写在src/main/resources目录下创建一个名为parallelApproval.bpmn20.xml的文件。这个文件名本身没有强制要求但.bpmn20.xml后缀是Activiti识别流程定义XML的约定建议保持。内容如下?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:activitihttp://activiti.org/bpmn targetNamespacehttp://activiti.example.org/parallel process idparallelApproval name并行审批流程 isExecutabletrue startEvent idstartEvent1 name开始/startEvent parallelGateway idforkGateway name拆分网关/parallelGateway userTask idfinanceTask name财务初审 activiti:assigneefinance_operator/userTask userTask idmanagerTask name项目经理审批 activiti:assigneemanager_operator/userTask parallelGateway idjoinGateway name汇聚网关/parallelGateway userTask idfinalTask name总经理终审 activiti:assigneedirector_operator/userTask endEvent idendEvent1 name结束/endEvent sequenceFlow idflow1 sourceRefstartEvent1 targetRefforkGateway/sequenceFlow sequenceFlow idflow2 sourceRefforkGateway targetReffinanceTask/sequenceFlow sequenceFlow idflow3 sourceRefforkGateway targetRefmanagerTask/sequenceFlow sequenceFlow idflow4 sourceReffinanceTask targetRefjoinGateway/sequenceFlow sequenceFlow idflow5 sourceRefmanagerTask targetRefjoinGateway/sequenceFlow sequenceFlow idflow6 sourceRefjoinGateway targetReffinalTask/sequenceFlow sequenceFlow idflow7 sourceReffinalTask targetRefendEvent1/sequenceFlow /process /definitions这个文件的流程结构是开始 - forkGateway拆成两条分支一条到财务初审、一条到项目经理审批两条分支完成后在joinGateway汇聚再进入总经理终审最后结束。isExecutabletrue表示这个流程定义可以被引擎执行不加也行但建议加上语义更明确。这里补充一个关于BPMN模型解析器的细节Activiti 6.0对并行网关节点的支持已经非常成熟部署时会把上述XML解析成内部的内存模型不需要额外引入DI图形化信息部分。如果你用可视化的建模工具生成文件通常会带BPMNDI坐标信息那部分对执行没有影响可以忽略。3.3 部署流程并启动实例流程定义文件写好之后启动引擎、部署流程、发起一个流程实例。我把这整个过程写成一个可直接运行的main方法。import org.activiti.engine.*; import org.activiti.engine.repository.Deployment; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; import java.util.HashMap; import java.util.List; import java.util.Map; public class ParallelGatewayDemo { public static void main(String[] args) { // 1. 构建独立引擎配置使用H2内存库 ProcessEngineConfiguration cfg ProcessEngineConfiguration .createStandaloneProcessEngineConfiguration() .setJdbcUrl(jdbc:h2:mem:activiti;DB_CLOSE_DELAY-1) .setJdbcDriver(org.h2.Driver) .setJdbcUsername(sa) .setJdbcPassword() .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE); ProcessEngine processEngine cfg.buildProcessEngine(); // 2. 部署流程定义 Deployment deployment processEngine.getRepositoryService() .createDeployment() .addClasspathResource(parallelApproval.bpmn20.xml) .name(并行审批流程) .deploy(); ProcessDefinition pd processEngine.getRepositoryService() .createProcessDefinitionQuery() .deploymentId(deployment.getId()) .singleResult(); System.out.println(部署成功, 流程定义ID pd.getId() , 版本 pd.getVersion()); // 3. 发起流程实例 MapString, Object variables new HashMap(); variables.put(title, 差旅报销单); variables.put(amount, 3500); ProcessInstance pi processEngine.getRuntimeService() .startProcessInstanceByKey(parallelApproval, BILL-2024-001, variables); System.out.println(流程实例启动成功, 流程实例ID pi.getId()); // 4. 查询当前所有待办任务 ListTask tasks processEngine.getTaskService() .createTaskQuery() .processInstanceId(pi.getId()) .orderByTaskCreateTime() .asc() .list(); System.out.println(启动后待办任务数 tasks.size()); for (Task task : tasks) { System.out.println( 待办任务: task.getName() , 处理人 task.getAssignee() , 任务ID task.getId()); } // 5. 完成第一条分支任务财务初审 Task financeTask processEngine.getTaskService() .createTaskQuery() .processInstanceId(pi.getId()) .taskName(财务初审) .singleResult(); processEngine.getTaskService().complete(financeTask.getId()); System.out.println(财务初审已完成); ListTask afterFirst processEngine.getTaskService() .createTaskQuery() .processInstanceId(pi.getId()) .list(); System.out.println(完成财务初审后待办任务数 afterFirst.size() , 当前流程实例是否已结束 (processEngine.getRuntimeService() .createProcessInstanceQuery() .processInstanceId(pi.getId()) .count() 0)); // 6. 完成第二条分支任务项目经理审批 Task managerTask processEngine.getTaskService() .createTaskQuery() .processInstanceId(pi.getId()) .taskName(项目经理审批) .singleResult(); processEngine.getTaskService().complete(managerTask.getId()); System.out.println(项目经理审批已完成); // 7. 验证汇聚后是否生成了最终任务 Task finalTask processEngine.getTaskService() .createTaskQuery() .processInstanceId(pi.getId()) .taskName(总经理终审) .singleResult(); System.out.println(汇聚发生后生成最终任务: (finalTask ! null) , 处理人 (finalTask null ? 无 : finalTask.getAssignee())); // 8. 完成最终任务并确认流程实例结束 processEngine.getTaskService().complete(finalTask.getId()); long runningCount processEngine.getRuntimeService() .createProcessInstanceQuery() .processInstanceId(pi.getId()) .count(); System.out.println(最终任务完成后流程实例是否已结束 (runningCount 0)); // 9. 关闭引擎 processEngine.close(); } }直接运行这个main方法控制台会输出类似下面这样的日志部署成功, 流程定义ID parallelApproval:1:4, 版本 1 流程实例启动成功, 流程实例ID 2501 启动后待办任务数 2 待办任务: 财务初审, 处理人 finance_operator, 任务ID 2504 待办任务: 项目经理审批, 处理人 manager_operator, 任务ID 2505 财务初审已完成 完成财务初审后待办任务数 1, 当前流程实例是否已结束 false 项目经理审批已完成 汇聚发生后生成最终任务: true, 处理人 director_operator 最终任务完成后流程实例是否已结束 true这段输出完整展示了并行网关的核心行为。最关键的是第5步之后财务初审完成时待办任务数还是1而且流程实例没有结束。很多人第一次跑并行流程到这里会怀疑自己是不是写错了其实没有。这是并行网关的汇聚机制在起作用第一条分支虽然完成但并行网关B还在等项目经理审批那条分支等第二条分支也到达之后汇合才正式发生引擎才会创建总经理终审任务。3.4 完成一个分支时引擎到底做了什么从执行实例的角度看流程启动之后引擎会在ACT_RU_EXECUTION表中创建多个执行实例记录。正常情况下你会看到一条根执行实例对应流程实例本身以及从拆分网关延伸出来的两条子执行实例。每个用户任务节点都挂在一条独立的子执行实例下面这样的结构是并行网关注并发的物理基础。当财务初审这个用户任务被complete时引擎会把对应子执行实例继续推进到汇聚网关。此时汇聚网关发现“进来的路”只走完了一条另一条分支还在跑于是它不会触发后续节点而是让当前执行实例在网关位置等待。从用户视角看就是“财务初审已办完流程没有任何变化”。这其实不是卡死而是并行网关的同步等待语义。当项目经理审批也被complete时第二条分支的执行实例同样推进到汇聚网关。汇聚网关检查到所有incoming分支都已经到达此时才真正执行“聚合”动作销毁这几条已经完成任务的分支执行实例让父执行实例继续向后流动创建总经理终审的用户任务。之后总经理终审完成整个流程实例运行结束运行时表里的执行实例和任务记录被清理。这种“先到先等最后一个到了才放行”的机制和现实生活中集合点等人是一模一样的先到的人不会走等到所有人都到了才一起出发。理解了这个模型你就理解了并行网关90%的行为。3.5 数据表层面怎么观察并行状态如果你把上面的Demo改成MySQL连接执行过程中去查几张核心表能看到更有意思的变化。启动流程后查ACT_RU_EXECUTION会看到同一个PROC_INST_ID_下面有好几条记录其中一条的PARENT_ID_为空这是主执行实例另外几条的PARENT_ID_指向主执行实例这是各并行分支。ACT_RU_TASK表里同时存在财务初审和项目经理审批两条待办任务说明任务确实是并发落下去的。只完成财务初审后再去查ACT_RU_EXECUTION属于财务分支的执行实例会移动到汇聚网关的位置记录仍然存在ACT_RU_TASK里少了一条任务但流程实例没有结束。这个状态最容易让监控系统产生误报如果你在项目里做流程监控要特别注意“流程实例未结束不代表流程卡死”先看是不是有分支任务还没处理完。最终汇聚完成后分支执行实例会被清理ACT_RU_EXECUTION里又只剩下主执行实例一条ACT_RU_TASK里出现总经理终审任务。流程结束之后ACT_RU_EXECUTION和ACT_RU_TASK里的相关记录都会被清空历史信息完整记录在ACT_HI_开头的表里。ACT_HI_PROC_INST的END_TIME_字段会写入结束时间ACT_HI_TASKINST里能看到每一步任务的开始和结束时间。这些历史表是日后做流程分析和统计的原始依据。4. 高频问题与实战避坑4.1 常见问题速查表我在项目里遇到的并行网关联问题基本可以汇总成下面这张表你可以贴到团队Wiki里当排查手册用。现象可能原因排查与解决方案启动后只生成一个待办任务并行网关出线只有一条或出线被误加了条件检查BPMN拆分支部分的出线数量和条件表达式完成一个分支后流程长时间没反应另一个并行分支尚未完成网关在等待汇聚查当前流程实例下所有待办任务找出未完成分支流程实例一直不结束某个分支执行实例挂在汇聚网关上没触发查ACT_RU_EXECUTION中等待节点为汇聚网关的记录并行网关出线上写条件不生效并行网关语义就是所有分支都执行条件会被忽略改成排他网关或包含网关兄弟分支读不到某个流程变量变量被设置到局部执行实例作用域隔离用complete(taskId, variables)传参或runtimeService.setVariable(processInstanceId, ...)高并发场景下任务重复创建部署了多个版本的流程定义启动时按key匹配到不同版本按deploymentId指定流程定义或检查版本号这张表里的每一行背后都有真实案例不是空泛的总结。尤其是“变量读不到”那个问题在并行分支里几乎是必现的值得单独展开讲。4.2 项目里的三个坑第一个坑是并行网关出线上写条件。某项目里有一个“金额大于5000走A分支否则走B分支”的需求设计人员把并行网关当成万能工具在两条出线上分别写了${amount 5000}和${amount 5000}。结果启动流程后两条分支全部执行了一次。原因我在前面已经说透了并行网关不读取分支条件它的职责就是全量放行。发现之后把并行网关改成排他网关问题立刻消失。这个坑初学容易踩所以设计评审的时候看到菱形带加号的网关一定要多问一句“这里真的不需要判断条件吗”。第二个坑是局部变量作用域。并行分支A在任务监听器里执行了delegateTask.setVariable(result, pass)分支B想读取result时发现是null。原因在于并行网关创建的子执行实例各自有独立的变量作用域直接set到当前任务环境的变量不会自动共享给兄弟分支。最稳妥的做法是完成任务时用complete(taskId, variables)把业务结果作为流程变量传进去或者明确用runtimeService.setVariable(processInstanceId, key, value)设置到流程实例级别。我在代码里已经用了这个模式实际开发时请务必遵守。第三个坑是外部系统调用失败导致流程半挂在汇聚点。并行分支里有两条分支各自调用不同的外部服务第一条成功后业务表已经写入第二条调用超时抛异常结果第一条分支的完成动作已经提交第二条分支的任务还停留在待办里流程整体表现为“卡在汇聚网关”。这种问题的本质是并行分支之间的进度相互独立引擎只能等所有分支都成功没法自动“跳过失败分支”。处理思路有两个方向要么在网关前面加排他或包含逻辑把失败分支引导到异常处理节点要么在外部调用不稳定的场景下把调用过程从用户任务的完成动作里剥离出来改成独立补偿机制避免一个分支失败拖住整条流程。4.3 并行分支数量与性能设计建议最后聊一下并行分支数量。理论上Activiti对并行网关出线数量没有硬性限制但在真实系统里我建议单次拆分的分支数控制在5条以内。原因很直接每个并行分支都对应一条ACT_RU_EXECUTION记录也会增加数据库锁的持有时间。如果这5条分支背后还挂着多个服务任务每个服务任务都做远程调用引擎事务压力和外部系统负载会同时上升。分支数量一多出现死锁的概率也会明显增加。嵌套并行更要当心。一个并行网关的某条分支里如果再套一个并行网关执行实例的父子层级会变得很深排查问题时看ACT_RU_EXECUTION的数据会非常费劲。如果业务上确实需要这种结构建议把内层的并行逻辑抽成一个独立的子流程让主流程保持清晰的单层并行结构。还有一点要提一下就是超时监控。做流程监控的时候不要只看流程实例的启动时间比如“超过3天没结束就报警”在并行场景下这是不准确的。正确做法是按分支任务维度去监控分别看每个ACT_RU_TASK的CREATE_TIME_哪个分支任务超时了就定位哪个分支先确认是处理人没做还是别的分支一直没完成再决定是催办还是干预。这个监控思路同样适用于其他会签类节点。我个人在实际项目里的体会是并行网关最忌讳的是“设计时觉得简单运行后没人盯”。它把一条流程变成了多条并发线索运行时问题定位的复杂度也随之上升。因此在把流程改造成并行之前先画一遍分支依赖图确认哪些环节真正需要并行哪些只是看着能并行实际存在隐式依赖再动手部署。这样跑起来之后你会少很多半夜查流程的麻烦。最后再分享一个小习惯每次修改并行相关流程定义后部署之前先在测试库里跑一遍“启动-完成所有分支-确认流程结束”的冒烟脚本我用这个习惯挡住过好几次低级的建模错误值得坚持。