自动步骤要不要占一个事务、要不要落作业队列,是流程引擎设计里最实际的分歧之一。本文从建模单元、触发时机、失败恢复与可观测性四个角度做对照,并给出实施侧的取舍建议。
NodeExt.ts 里 WhoExeIt 是节点属性面板上的一个系统枚举下拉:
@0=操作员执行 @1=机器执行 @2=混合执行 @3=流水线执行 @4=过程执行它回答的问题是「这个节点由谁做完」。其中 4 就是过程执行(过程节点):
对照该平台自己的另外几种执行方式,就能看清过程节点的定位:
WhoExeIt | 名称 | 触发时机 | 是否进普通待办 |
|---|---|---|---|
0 | 操作员执行 | 人点发送 | 进 |
1 | 机器执行 | 后台定时扫描 | 不进(查询排除) |
2 | 混合执行 | 打开表单时先试发 | 失败才进 |
3 | 流水线执行 | 工位扫码发送 | 进独立工位通道 |
4 | 过程执行 | 当次发送请求内连发 | 不进 |
关键在最后一行:过程节点和机器节点都不是「人」,但它们的触发路径不同。机器节点把继续流转交给定时任务(AutoRun_WhoExeIt.cs 里那条 WHERE WhoExeIt IN (1,2) 的 SQL,明确把 3、4 排除掉);过程节点则在发送线程里当场串起来跑完。
发送生成待办时,引擎把节点上的值抄到待办行(WorkNode.cs):
wl.WhoExeIt = town.HisNode.WhoExeIt; // 设置谁执行它批量的插入语句同样带上这一列(WorkNodePlus.cs):
INSERT INTO WF_GenerWorkerList (WorkID,FK_Emp,FK_Node,Idx,AtPara,IsEnable,WhoExeIt) VALUES(...)所以 WhoExeIt 不是设计期的装饰字段,而是运行时调度依据:待办查询、定时任务、消息推送、流水线列表、过程页标签读的都是它。
过程节点不进定时任务,由发送时同步连发。逻辑可以概括为:
发送完成后
while 下一节点的 WhoExeIt == 4 且流程未结束 且存在待办:
切换到该待办处理人(Port_Login)
以系统身份走完整发送(方向条件、接收人、发送前事件仍生效)
把本次过程返回叠加进总消息
切回原操作员「同步」两个字的含义是:这一次 HTTP 请求不返回,直到这一串过程节点跑完、或者某一步抛错停下。这跟机器节点「先落一条待办、后台慢慢发」是两条完全不同的路径。
AutoRun_WhoExeIt.cs 里的注释写得很直白:
// 只捞机器执行(1)、混合执行(2)。流水线(3)、过程执行(4)不能进定时任务:4 由发送时同步连发。
string sql = "SELECT WorkID,FID,FK_Emp,FK_Node,FK_Flow,AtPara FROM WF_GenerWorkerList WHERE WhoExeIt IN (1,2) AND IsPass=0 AND IsEnable=1 ORDER BY FK_Emp";这条边界非常重要:配置人员如果把一串连续系统节点误设成机器执行(1),它们就会变成一条条等待扫描的待办,时延、事务边界、失败位置都和过程执行(4)不一样。 这是实施时最容易踩的坑。
过程节点若仍弹一句「发送成功」,集成场景基本没法用——外部系统可能跑几十秒,人不知道卡在哪一步、返回了什么。该引擎用节点属性 发送后转向(6/7) 接上过程执行页(MyProNodes.vue):
WhoExeIt),前端据此打标签:// 前端模块源码目录
const exeLabel = (whoExeIt?: number) => (Number(whoExeIt) === 4 ? '[过程执行]' : '[人工执行]');后端在取待办数据时专门补了一张表,把节点的 WhoExeIt 带给前端:
// ProNodes:全流程节点含 WhoExeIt,用于时间轴 [过程执行]/[人工执行] 标签
Nodes flowNodes = new Nodes(this.FlowNo);
ds.Tables.Add(flowNodes.ToDataTableField("WF_FlowNodes"));人看到的不再是黑盒自动任务,而是人机混合轨迹:人工节点显示处理人头像,过程节点显示系统返回与等待时长。
这是设计上必须强调的一点:连发过程仍走完整发送——ExecEvent.DoNode(EventListNode.SendWhen, ...) 照常触发,方向条件、接收人规则、阻塞模式全部生效(见 WorkNode.cs NodeSend)。身份切到待办处理人再切回,审计主体仍然是人,而不是一个匿名的「引擎账号」。
外部 API 失败时,抛出带「系统执行发送」前缀的异常,流程停在具体那个过程节点,前端过程页展示失败节点与返回原文。对国内审批留痕的习惯来说,这比「引擎账号执行、活动上无办理人」更贴近预期。
BPMN 2.0 的答案不是「节点的执行方式」,而是换一种活动类型:
BPMN 图元 | 职责 | 与该引擎大致对等 |
|---|---|---|
User Task | 人工等待,TaskService 查询/签收/完成 | 操作员执行(0) |
Service Task + 类/表达式 | 引擎内同步调用 Java 逻辑 | 过程执行(同步部分) |
Service Task + asyncBefore | 落作业,Job Executor 续跑 | 机器执行(1) |
External Task(C7/Flowable)/ Job Worker(C8) | 外部进程按类型拉取、加锁、完成/失败 | 无直接一等对等 |
Timer Event | 到点继续 | 延期发送 / 机器执行 |
Script / Business Rule Task | 脚本或决策表 | 无独立图元 |
同一张流程图上,人做的、系统做的、外部系统做的是不同的图形元素。图上信息量大、语义精确,代价是实施人员要熟悉规范。
同步委托(camunda:class / flowable:class):引擎事务里直接调用 Java 代码。这跟该引擎过程执行的「同一次请求内连发」最接近——都在调用方线程/事务里把逻辑跑完。
异步延续(asyncBefore="true" / flowable:async="true"):到达活动时先提交当前事务、落一条作业,返回调用方,由 Job Executor 用独立事务、独立线程续跑。官方文档对它的定位是「引入事务边界 / 保存点」,而不只是「让节点自动跑」:
By setting the
asyncBefore='true'attribute we introduce an additional save point at which the process state will be persisted and committed to the database. A separate job executor thread will continue the process asynchronously by using a separate database transaction.
Flowable 的作业在 API 调用期间就写入 ACT_RU_JOB 表,成功的提交会触发 Async Executor 执行;执行器有可配置线程池,并有「acquire」线程去抢占未被加锁的作业。Camunda 7 的 JobExecutor 默认使用 exclusive job:同一流程实例的排他作业同一时刻只执行一个,避免并行合流处产生乐观锁冲突。
失败处理是这套模型的核心价值:
camunda:failedJobRetryTimeCycle 配成 R5/PT5M 这类策略);BpmnError → 跳过重试,交给边界错误事件;对照该引擎:机器执行用「扫待办、发一次、失败记日志」近似「有活就发」,没有同等的作业锁协议、重试退避和事故工作台。社区/集群部署下要靠部署约定而不是引擎级 job lock。这一点对比必须写明:在高并发自动节点与失败恢复上,BPMN 作业模型更完整。
External Task 的类比是:服务任务也是一种待办,只是领取者不是人而是程序。fetchAndLock → 做事 → complete / failure,工人可多语言、可水平扩展,引擎不持有长调用。
Camunda 8(Zeebe)把这条路走得更彻底:Service Task 到达即生成一条持久化 job,worker 用 ActivateJobs(带 type、worker、timeout、maxJobs)拉取,再回 CompleteJob / FailJob(递减重试、可设 backoff)/ ThrowError。超时会重新分配作业,因此投递语义是 at-least-once,处理逻辑必须幂等。文档明确指出:worker 与引擎之间不存在横跨两边的技术事务,要靠业务事务来兜一致性。
这条路的代价是运维更重:要跑一套消费者、要处理超时与重试、要保证幂等。换来的是跨语言、长耗时、可独立扩缩的集成能力。
维度 | 该引擎(WhoExeIt=4) | Flowable / Camunda 7 | Camunda 8(Zeebe) |
|---|---|---|---|
建模方式 | 同一节点改属性 | 换 BPMN 图元 | 换 BPMN 图元 |
过程执行的默认形态 | 发送请求内同步连发 | Service Task 同步委托 | 默认走 Job Worker |
异步执行载体 | 待办行(WhoExeIt 区分) | ACT_RU_JOB | 持久化 job 队列 |
调度者 | 发送线程 / 定时任务 | Job Executor | Worker 轮询 / 推送 |
作业锁 | 无引擎级锁 | exclusive job | job 独占 + 超时重分配 |
重试 | 需再次发送或人工处理 | 引擎自动重试 | worker 上报递减重试 + backoff |
失败落点 | 具体过程节点,返回带异常原文 | 作业/事故(Incident) | job 失败/事故,Operate 可见 |
人怎么看 | 业务侧过程执行页(时间轴/流程图) | 运维侧 Cockpit / 历史活动 | 运维侧 Operate |
事务边界 | 一次请求内可能很长 | asyncBefore 切分保存点 | 每步独立提交、at-least-once |
典型优势 | 同一套节点生命周期、配置面直接、过程可视化给业务看 | 规范、作业可靠、生态大 | 分布式、多语言工人、水平扩展 |
该引擎的过程节点仍是胖节点:接收人、方向条件、退回、撤销、轨迹、表单权限都挂在它身上。把过程节点从「操作员」改成「过程执行」,这些能力一个都不用重配。
BPMN 里 Service Task 是瘦活动:它天然没有办理人、没有中国式审批的退回/加签/抄送。要在自动步骤上做这类事,得靠监听器、执行表达式或扩展属性自己补。
这不是谁优谁劣,而是复杂性放在了哪里:
WorkNode.cs 里对机器节点显式短路:if (this.town != null && this.town.HisNode.WhoExeIt == 1)
return; // 机器执行不推到达消息该引擎过程执行优化的是「人点一次发送、盯着集成跑完」。它把一串系统节点串在同一次请求里,业务上等价于「这一批动作是一件事」。
代价同样清楚:
Camunda 7 的官方建议恰恰相反:即便一串自动活动可以在同一事务里跑完,也建议用 asyncBefore / asyncAfter 切开,避免长事务与锁竞争。Flowable / Camunda 用作业把外部 IO 移出引擎事务,这是多年踩坑后的默认结论。
环节 | 该引擎的过程执行 | Flowable / Camunda 的异步作业 |
|---|---|---|
起点 | 人工点发送 | 人工完成任务并提交 |
第 1 步 | 过程节点 A,同一请求内连发 | 提交事务,落作业 1 |
第 2 步 | 过程节点 B,同一请求内连发 | 独立事务执行活动 A,落作业 2 |
第 3 步 | 过程节点 C,同一请求内连发 | 独立事务执行活动 B,落作业 3 |
收尾 | 汇总结果返回调用方 | 独立事务执行活动 C |
事务边界 | 整条链路一个事务 | 每个活动一个保存点 |
失败后 | 停在具体过程节点,返回异常原文 | 作业重试,耗尽后生成事故 |
左边一条链在同一事务里走完,快、直观、可视化好;右边每一步都有保存点,崩了能从作业恢复,但多了调度与运维成本。
左边一条链在同一事务里走完,快、直观、可视化好;右边每一步都有保存点,崩了能从作业恢复,但多了调度与运维成本。
该引擎过程执行 | Camunda 同步委托 | 异步作业 / Worker | |
|---|---|---|---|
失败位置 | 当前过程节点,返回带「系统执行发送」前缀 | 当前活动(事务回滚范围取决于实现) | 作业/工人失败,实例停在该活动 |
重试 | 需再次发送或人工处理 | 同请求内难自动重试 | 引擎/worker 重试 + 事故 |
人怎么看 | 业务侧过程执行页失败态 + 返回原文 | 异常栈 / 业务自绘 | Operate / Cockpit(运维视角) |
过程执行页把集成报文展示给业务人员;Cockpit / Operate 面向运维人员。两者不互相替代。
场景 | 更贴该引擎的用法 | 更贴 Flowable / Camunda 的用法 |
|---|---|---|
人点发送后连续调内部 API,要盯进度 | 过程执行(4)+ 发送后转向 6/7 | 一串 Service Task + 自建进度页;要可靠则每步 async |
夜间过账、不占待办 | 机器执行(1) | asyncBefore Service Task |
打开即过、过不了再填 | 混合执行(2) | 网关拆分,或监听器 Complete |
工位扫码过站 | 流水线执行(3) | User Task + 自研工位端 |
跨语言、长耗时、需重试扩缩 | 不宜单靠过程同步连发 | External Task / Job Worker |
到点再发已填好的单 | 延期发送(改执行方式) | Timer 事件(图上可见) |
混合使用也常见:核心审批用该引擎,超长集成用独立工人回写后「发送」推进。那是架构组合,不是某引擎的失败。
三家都在解决同一件事:人工等待与系统继续如何在一条流程里共存。分歧不在「有没有自动节点」,而在建模单元和调度哲学:
实现上可以记成:同一节点、五种执行方式、同一套发送。机器靠待办扫描,过程靠当次连发,混合靠打开试发,流水线走工位接口;WhoExeIt 同时管待办过滤、消息短路和轨迹标签。
过程节点(WhoExeIt=4)是这套设计里最典型的一个切面:它把「一串系统动作」仍然当成节点来管——有接收人、有事件、失败停在具体节点、业务人员能在过程页看见人机混合轨迹。而 Flowable / Camunda 用作业模型换来了可恢复、可重试、可多语言扩展的自动步骤。复杂性放的位置不同,验收标准就不该混用。
文件 | 作用 |
|---|---|
前端模块源码目录 | WhoExeIt 前端属性定义(@4=过程执行) |
前端模块源码目录 | 设计器枚举与写回节点 |
引擎源码模块 | 后端节点实体枚举定义 |
引擎源码模块 | WhoExeIt 字段常量 |
引擎源码模块 | Node.WhoExeIt 取属性值 |
引擎源码模块 | 待办抄写 WhoExeIt、发送前事件、机器节点消息短路 |
引擎源码模块 | 待办批量插入带 WhoExeIt |
引擎源码模块 | 机器/混合节点定时执行(排除 3、4) |
引擎源码模块 | 下发全流程节点 WhoExeIt 给过程页 |
前端模块源码目录 | 过程执行页时间轴 / 流程图 / 标签 |
引擎源码模块 | 流水线(WhoExeIt=3)待办接口 |
引擎源码模块卫星设计院任务流程控制系统/...xml | 实际流程中过程节点(WhoExeIt=4)的配置样例 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。