引言
在上一篇中,我们从硬编码的痛点出发,明白了工作流引擎的核心价值是解耦,并抽象出了"流程定义(地图)"、"流程实例(火车)"和"任务(车站)"三大基础概念。
但是,这仅仅是外壳。
作为技术人,我们最关心的是:这台引擎的"发动机"到底是怎么转起来的?
试想一个场景:主管王五在请假系统里点了一下"同意"按钮,前端发了一个请求给后端。
此时,工作流引擎内部究竟发生了什么?它怎么知道下一步该去哪?如果流转到一半服务器突然停电宕机了,数据会乱吗?
今天,我们就扒开工作流引擎的底层架构,看看它是如何精密运转的。
1 工作流引擎的底层架构:从Token到数据库的精密运转
当王五点击"同意"时,他其实是调用了引擎的一个类似 completeTask(taskId) 的方法。
此时,引擎面临的首要问题是:当前节点结束了,下一步该去哪?
1.1 图的遍历与"Token(令牌)"机制
所有的工作流地图(BPMN XML),在引擎内部最终都会被解析成一个数据结构:有向图(Directed Graph)。
为了在这个图里"走路",工作流引擎引入了一个极其核心、也是最精妙的概念:Token(令牌) 。
在一些开源框架(如Flowable/Camunda)的源码中,它被称为 Execution(执行实例)。
你可以把 Token 想象成一个在迷宫里探险的小人。
- 当流程启动时,引擎在"开始节点"生成一个 Token
- Token 顺着连线(Sequence Flow)走到"主管审批"节点,停下来,并生成一个待办任务给王五。此时 Token 处于休眠状态
- 王五点击"同意",唤醒了 Token
- 引擎拿到当前 Token 所在的节点,读取流程定义的"图",发现只有一条出线(Outgoing)指向"HR审批"
- 于是引擎把 Token 移动到"HR审批"节点,再次休眠,并生成 HR 的待办任务
1.2 遇到分叉路口怎么办?(网关与Token分裂)
如果是单线流程,一个 Token 就够了。但真实的业务充满了并行和分支。
1.2.1 排他网关(Exclusive Gateway)------ 走哪条路?
类似于代码里的 if-else。Token 走到网关时,引擎会计算连线上的条件(比如 days > 3)。一旦某条线的条件满足,Token 就顺着那条线走,其他线直接忽略。
1.2.2 并行网关(Parallel Gateway)------ 影分身之术!
比如一个新员工入职流程,需要"IT部发电脑"和"行政部发工牌"同时进行 。
这个时候,奇迹出现了:
当主 Token 走到并行网关时,它会分裂(Fork) !
主 Token 停在原地,分裂出两个子 Token(Token-A 和 Token-B),分别飞向 IT部 和 行政部节点。
此时,系统里同时存在两个待办任务,互不阻塞。
那什么时候合流呢?
当 IT部 和 行政部 都处理完后,Token-A 和 Token-B 会走到"并行合并网关"。引擎会检查:所有分支的 Token 都到了吗?
如果 Token-A 先到,它就停在那里等。直到 Token-B 也到了,引擎把它们俩销毁,唤醒主 Token 继续往下走。
这就是工作流引擎能轻松处理复杂并发的底层秘密! 业务代码里根本不需要写多线程和锁,引擎用 Token 的分裂与合并完美解决了。
2 核心问题二:服务器宕机了怎么办?(持久化机制)
前面说的 Token 走来走去,听起来很像是在内存里运行的。
但请假流程动辄几天、几周,如果 Token 放在内存里,服务器一重启,张三的请假单不就灰飞烟灭了吗?
这就引出了工作流引擎的第二个核心:它本质上是一个由关系型数据库(MySQL/Oracle)强力驱动的状态机。
所有的引擎(无论是Activiti、Flowable还是Camunda),底层都有几十张精心设计的数据库表。为了不让你晕,我们把它们分为三大阵营:
2.1 静态定义表(Repository)
这里存的是"地图"。你画好的 XML 文件、流程的名字、版本号,都存在这里。这些数据一旦部署,基本上是只读的。
2.2 运行时表(Runtime)------ 绝对的核心!
这里存的是"正在跑的火车和当前的车站"。
刚才说的 Token(执行实例) 和 待办任务(Task),就存在运行时表里。
- 引擎的极速秘诀 :运行时表的数据量永远是很小的。当一个任务被完成后,引擎会立刻从运行时表中物理删除这条记录。这样保证了引擎在查询"我的待办任务"时,速度永远极快。
2.3 历史表(History)------ 审计的基石
既然运行时表会被删,那怎么查流程的流转记录呢?
答案是历史表。引擎在把数据写入/删除运行时表的同时,会往历史表里 INSERT 一份快照。这里记录了流程从头到尾的所有足迹,用于业务系统的"审批历史轨迹"展示。
2.4 宕机如何保证数据一致?(事务绑定)
回到王五点击"同意"的瞬间。引擎在底层其实执行了类似如下的 SQL 逻辑,并且全部包裹在一个数据库事务(Transaction)中:
sql
BEGIN TRANSACTION;
-- 1. 更新 Token 的位置到下一个节点
UPDATE ACT_RU_EXECUTION SET ACT_ID_ = 'HR_Approval' WHERE ID_ = 'token_123';
-- 2. 删除王五当前的待办任务
DELETE FROM ACT_RU_TASK WHERE ID_ = 'task_456';
-- 3. 插入下一个 HR 的待办任务
INSERT INTO ACT_RU_TASK (ID_, NAME_, ASSIGNEE_) VALUES ('task_789', 'HR审批', 'hr_user');
-- 4. 记录历史操作日志
INSERT INTO ACT_HI_ACTINST ...
COMMIT;
如果执行到第3步时服务器突然断电,数据库事务会回滚。
重启后,数据库里依然是王五的任务,Token 还在原来的位置。一点数据都不会丢,完美的一致性!
3 核心问题三:引擎如何与我们的业务数据绑定?
现在引擎内部转得很欢快了,但暴露出了最后一个致命问题:
引擎是个瞎子。
它只知道:"流程实例 1001 走到了任务 5002,处理人是 张三"。
但它根本不知道:"这是谁的请假单?请了几天?理由是什么?"
如果不解决这个问题,业务系统和引擎就是割裂的。为了搭建这两座桥梁,引擎提供了两个极其重要的机制:
3.1 业务键(Business Key)------ 身份的绑定
当你的业务系统往 leave_request 表插入一条请假单记录(假设主键 ID 是 9527)后,你在启动工作流时,必须把这个 9527 告诉引擎。
java
// 伪代码:启动流程,并传入 Business Key
engine.startProcessInstanceByKey("leaveProcess", "9527");
引擎会把 9527 存到自己的流程实例表中。
以后,当张三打开"我的待办"列表时,引擎查出了一堆 Task,前端拿到 Task 对应的 Business Key(9527),再去你的业务表里 SELECT * FROM leave_request WHERE id = 9527,就能把请假详情展示出来了。
3.2 流程变量(Process Variables)------ 决策的依据
前面说到,排他网关需要根据 days > 3 来判断走哪条路。引擎怎么知道 days 是多少?
你需要在流程流转的过程中,把业务数据作为**变量(Variables)**塞进引擎里。
java
Map<String, Object> variables = new HashMap<>();
variables.put("days", 5);
variables.put("applicant", "zhangsan");
// 完成任务,并把变量告诉引擎
engine.completeTask(taskId, variables);
引擎会把这些变量序列化后存入数据库(甚至有专门的变量表)。当 Token 走到网关时,引擎内置的表达式解析器(比如 JUEL 表达式)就会取出 days 的值进行计算,从而决定 Token 的走向。
4 总结与思考
至此,我们彻底看清了工作流引擎运转的底层逻辑:
- 执行机制 :将流程图转化为有向图,利用 Token(令牌) 的移动、分裂、合并来驱动流程流转
- 持久化与一致性 :依托关系型数据库 和本地事务,通过运行时表和历史表的读写分离,保证了引擎的高性能和抗宕机能力
- 业务融合 :通过 Business Key 绑定业务数据,通过 流程变量 驱动网关路由,实现了引擎与业务系统"松耦合"的完美配合
然而,现实总是残酷的。
以上讲的,都是理想状态下的"正向流转"。
但在中国特色的企业管理中,领导们的要求往往千奇百怪:
- "这个节点我要加个人一起审批!"(动态加签)
- "我点错了,我要把流程退回到上一步!"(驳回)
- "我要把流程直接退回到发起人!"(任意节点跳转)
- "我们三个人审批,只要有两个人同意就算过!"(复杂会签)
BPMN 2.0 规范主要源自西方,很多中国特色的复杂流转逻辑,原生引擎根本不支持!
在下一篇**《工作流引擎系列(三):中国式复杂工作流的破局与实战》**中,我们将直面这些令人头秃的"变态需求",教你如何基于开源引擎进行改造和扩展,真正将工作流引擎落地到复杂的企业级项目中。