☰
Activiti运行时表ACT_RU_全解析:流程引擎实时状态与问题排查指南
2026/10/15 1:22:24 网站建设 项目流程

Activiti的运行时数据表,也就是ACT_RU_开头这一批“跑着的表”,是流程引擎在工作过程中的实时状态记录。我学习Activiti时最深的感受是:画BPMN图不难,难的是流程跑起来之后,你不知道当前停在哪、为什么停、该去查哪张表。后来我花了不少时间把这批表全部过了一遍,再回头看问题时,基本都能从数据上解释清楚。这篇内容就围绕运行时数据表展开,聊聊每张表的职责、数据变化时机,以及我在实际项目里是怎么用它们排查问题的。适合刚开始接触Activiti的朋友,也适合正在做流程引擎二次开发、或者处理线上流程卡住问题的同学。

1. 先建立全局观:运行时表是流程引擎的“实时调度面板”

Activiti的数据库表按前缀分三大类:ACT_RE_存流程定义等静态资源,ACT_HI_存历史数据,ACT_RU_存运行时数据。“运行时”三个字的意思就是“正在跑”。你可以这样理解:业务表记录业务单据本身的状态,而ACT_RU_表记录流程引擎内部每时每刻的推进位置。如果说业务系统是“货”,流程引擎就相当于调度室,ACT_RU_表就是调度室墙上的调度面板——哪辆车在哪个路口、哪个司机在等什么,全写在上面。

为什么需要一套独立的运行时表,而不是直接把这些状态写进业务表?因为流程推进是异步且动态的:一个流程实例可能同时存在多个活跃分支、多个待办任务、等待中的定时器、等待外部信号的事件。这些信息变化极其频繁,和业务数据混在一起,要么产生锁竞争,要么让业务表结构越来越复杂。把运行状态单独拆出来,引擎才能高效地更新、查询和控制这些临时状态。

1.1 一张表一张职责:ACT_RU_表全集速览

表名职责数据特征
ACT_RU_EXECUTION流程实例与执行实例的树状关系一个流程可能多行,根行PROC_INST_ID_为空
ACT_RU_TASK当前待办任务任务创建即插入,完成即删除
ACT_RU_VARIABLE运行中的流程变量区分流程级、执行级、任务级
ACT_RU_IDENTITYLINK任务与办理人、候选人的关系候选、签收、委派都记录在这里
ACT_RU_EVENT_SUBSCR等待中的中间捕获事件信号、消息事件的订阅信息
ACT_RU_JOB / TIMER_JOB / SUSPENDED_JOB / DEADLETTER_JOB异步作业与定时任务按状态拆成多张表,避免相互锁竞争
ACT_RU_INTEGRATION服务集成调用信息部分版本才有,用于集成场景

我见过不少同事只盯着ACT_RU_TASK和ACT_RU_EXECUTION看,等到流程卡在定时器或者消息事件上时,完全不知道去哪查。其实把上面这张表记住,排查问题的方向就不会跑偏。

1.2 用最简单流程跑一遍,看数据落表顺序

假设一个流程:开始 -> 填写申请(用户任务)-> 审批(用户任务)-> 结束。我强烈建议初学者用这种方式建立直觉:启动流程实例,ACT_RU_EXECUTION增加1行,根执行实例的PROC_INST_ID_为空。到达“填写申请”任务节点,ACT_RU_TASK增加1行,任务ID和执行ID关联。用户提交表单并调用runtimeService.setVariable,ACT_RU_VARIABLE增加1行。任务完成、进入审批节点,ACT_RU_TASK删除原任务、新增审批任务。审批人点击完成,流程走到结束事件,ACT_RU_TASK被删除,ACT_RU_EXECUTION里的行也随之清理。整个过程结束后,运行时表里不会残留这个流程的数据。

这个过程就是Activiti运行时数据的基本生命周期。你只要亲手跑一遍这种流程,把每个动作对应的表变化记录下来,后面看任何复杂流程都会觉得熟悉。

2. ACT_RU_EXECUTION:一个流程实例为什么在这里留多行

ACT_RU_EXECUTION是理解Activiti运行机制最重要的一张表,也是最容易让人困惑的。很多初学者看到同一张表里一个流程实例对应好几行,就以为数据写重了。实际上这是正常现象,关键要搞清楚根执行和子执行的区别。

2.1 根执行与子执行:进程和线程的类比

流程实例(Process Instance)和执行实例(Execution)是不同的概念。我用进程和线程来类比:流程实例相当于一个进程,是整体运行的主标识,业务上通常用来关联业务主键;执行实例相当于线程,代表流程路径上实际向前推进的分支。一个进程里可以有多个线程,一个流程实例里也可以有多个执行实例。

在ACT_RU_EXECUTION中,只有一行是“根执行实例”,它的PROC_INST_ID_字段为NULL,ID_和流程实例ID一致。其余行的PROC_INST_ID_指向根执行实例的ID,表示子执行。并行网关产生的每个并行分支、子流程产生的新执行、多实例会签产生的新执行,都会作为子执行出现。查询时如果只按PROC_INST_ID_过滤,会得到一堆行,稍不留神就把它们的含义理解偏了。

2.2 并行网关前后,表里的数据怎么变

流程:开始 -> 并行网关 -> 分支A审批 -> 分支B审批 -> 并行汇合 -> 结束。流程启动后,ACT_RU_EXECUTION插入1行根执行。进入并行网关,引擎把根执行停住,创建两个子执行,每个子执行挂一个用户任务,两个任务处于并行状态。分支A完成,对应子执行的IS_ACTIVE_变为0或被清理;分支B完成同理。两个分支都完成后,并行网关汇合,根执行继续往下走,子执行的行被彻底清掉。

所以,“某流程现在停在哪”不能简单按PROC_INST_ID_过滤后取第一行,而要看所有IS_ACTIVE_=1的行,找到各个活跃执行点。我查线上问题时最常用这条SQL:

SELECT ID_, PROC_INST_ID_, PARENT_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, IS_SCOPE_ FROM ACT_RU_EXECUTION WHERE PROC_INST_ID_ = #{procInstId} OR (PROC_INST_ID_ IS NULL AND ID_ = #{procInstId})

2.3 字段速查与易错点:根行PROC_INST_ID_为空

ACT_RU_EXECUTION里值得重点关注的字段包括:PARENT_ID_表示父执行,ACT_ID_表示当前执行停靠的BPMN元素ID,IS_ACTIVE_表示是否活跃,IS_CONCURRENT_表示是否是并发分支,IS_SCOPE_表示是否承担作用域职责,SUSPENSION_STATE_表示流程是否挂起。

最容易搞错的有两处:一是根执行的PROC_INST_ID_是NULL,别用“WHERE PROC_INST_ID_ = 自己”去查根行,否则查不到;二是高版本还引入了ROOT_PROC_INST_ID_,用来追溯嵌套子流程的最外层实例。理解了执行树的概念,ACT_RU_EXECUTION这张表就基本通关了。

3. ACT_RU_TASK 与 ACT_RU_IDENTITYLINK:任务和人的关系拆开存

用户任务是Activiti最常见的停留节点。ACT_RU_TASK记录当前所有未完成的任务,“当前”两个字很关键,任务完成时行会被删除,历史任务则落在ACT_HI_TASKINST里。

3.1 任务表核心字段和写入时机

ASSIGNEE_是当前办理人,OWNER_是任务所属人。TASK_DEF_KEY_对应BPMN流程图中用户任务节点的定义ID,方便反查流程模型。EXECUTION_ID_和PROC_INST_ID_把任务绑定到执行实例和流程实例。CREATE_TIME_是任务创建时间,DUE_DATE_是截止时间,SUSPENSION_STATE_表示流程挂起时任务是否被同时挂起。

有一个细节我来回提醒同事:用户任务创建时,如果BPMN里指定了activiti:assignee,ASSIGNEE_会直接写入;如果指定的是候选组或候选人,ACT_RU_TASK中ASSIGNEE_为空,候选人信息写入ACT_RU_IDENTITYLINK。签收动作的本质就是调用claim方法,把ASSIGNEE_设置为当前用户,同时删除该任务相关的候选关系。

3.2 身份关系表:候选、签收、委派的变化

ACT_RU_IDENTITYLINK里有几个关键字段:USER_ID_、GROUP_ID_、TYPE_、TASK_ID_、PROC_INST_ID_。TYPE_常见的有candidate、assignee、owner、participant。一个任务指定了三个候选人,IDENTITYLINK里会有三行TYPE_=candidate的记录。候选人签收后,这三行会被清理,任务表里的ASSIGNEE_被更新。如果做了委派,原办理人变成OWNER_,受派人写ASSIGNEE_,TYPE_关系跟着变。

线上查某人待办,如果不走API,可以这样查:

SELECT t.ID_, t.NAME_, t.TASK_DEF_KEY_, t.CREATE_TIME_ FROM ACT_RU_TASK t WHERE t.ASSIGNEE_ = #{userId} OR EXISTS ( SELECT 1 FROM ACT_RU_IDENTITYLINK l WHERE l.TASK_ID_ = t.ID_ AND l.USER_ID_ = #{userId} AND l.TYPE_ = 'candidate' )

3.3 子任务和多实例会签在任务表里的模样

PARENT_TASK_ID_字段表示子任务,父子任务关系通常出现在子流程、任务嵌套、“主任务+子步骤”等场景。会签任务则是多实例节点,每个实例会生成独立的执行和独立的任务记录,ACT_RU_TASK里会出现多个TASK_DEF_KEY_相同的任务,但它们各自挂在不同的子执行实例上。排查会签问题如果只盯着任务表,很容易漏掉执行实例的结构,这也是我建议把第二章和第三章放在一起看的原因。

4. ACT_RU_VARIABLE:变量存储、作用域与序列化的坑

流程变量是业务和引擎之间传递数据的核心通道。ACT_RU_VARIABLE的表结构很有意思,它不是简单地把变量塞进一个大字符串列,而是用多列按类型分存。

4.1 类型化列设计:为什么把变量拆成多列

TYPE_字段记录变量的Java类型,如string、integer、long、boolean、date、double、serializable等。TEXT_、TEXT2_存字符串类数据,LONG_存long和int,DOUBLE_存浮点数。序列化对象的数据放在ACT_GE_BYTEARRAY表里,ACT_RU_VARIABLE只保留BYTEARRAY_ID_引用。

这个设计的好处是引擎按类型存取时直接走对应列,不同变量互不干扰。坏处是,如果你把一个大对象塞进去,ACT_GE_BYTEARRAY会迅速膨胀,而且反序列化时对类非常敏感。早期我看到有人把整个业务对象都存进流程变量,运行一段时间后,变量表比业务表还大,查询性能明显下降。

4.2 三个作用域:流程级、执行级、任务级

这是初学者容易踩的坑。runtimeService.setVariable设的是流程实例变量,绑在根执行实例上;在某个子执行上调用executionService设置变量,变量绑在具体执行实例上;taskService.setVariableLocal设的是任务本地变量,ACT_RU_VARIABLE中TASK_ID_字段会指向任务。

任务级变量在任务完成后很可能被清理,流程级变量则跟到流程结束。我的建议是:需要跨节点共享的数据用流程级变量,只在某个任务内短暂使用的数据用任务级变量,用完及时清理,避免运行时表越积越胖。对象到底绑定在哪一层,直接影响删除和查询逻辑,调试时可以用TASK_ID_、EXECUTION_ID_、PROC_INST_ID_三个字段反推。

4.3 自定义对象序列化的兼容性教训

我在某项目里吃过亏:流程变量里存了自定义对象,后来重构改了包名和类结构,老流程实例再次读取变量时直接抛ClassNotFoundException,线上流程全部卡死在同一个节点。最后只能写脚本把变量改回基本类型,再让流程继续跑。

从那以后我定了个规矩:能存基本类型、字符串、日期的,绝不存自定义对象;非要存对象,也要留好版本兼容字段。另外,高并发下对同一流程实例反复setVariable会触发乐观锁REV_冲突,报OptimisticLockingException,所以批量写变量时尽量合并成一次调用,减少无谓的锁竞争。

5. 作业表家族与事件订阅表:异步和等待的数据形态

流程引擎的不少逻辑不是同步跑完的,而是靠异步作业或事件等待驱动。这些机制的落表就是作业表和事件订阅表。

5.1 作业表为什么从一张拆成多张

老版本里所有异步任务都集中在ACT_RU_JOB一张表,后来拆分出了ACT_RU_TIMER_JOB、ACT_RU_SUSPENDED_JOB、ACT_RU_DEADLETTER_JOB等。拆分的直接原因是定时器和异步任务混在一张表会互相争抢扫描,锁竞争严重。定时器作业的DUEDATE_决定触发时间,普通异步作业则几乎立即执行。作业执行失败会带着异常信息和重试次数,重试N次后进入死信表。

排查作业问题,我会先看LOCK_OWNER_和LOCK_EXP_TIME_:LOCK_OWNER_是当前持有作业锁的引擎节点标识,LOCK_EXP_TIME_是锁过期时间。作业一直不执行且锁没过期,说明作业被某个节点锁住但没完成;锁过期了还不动,就要怀疑执行器没启动或线程池被打满。

5.2 定时器边界事件从插入到触发的全过程

举个例子:审批节点加了“3天超时自动通过”的定时器边界事件。流程到达该节点时,引擎会同步创建用户任务,同时在ACT_RU_TIMER_JOB插入一条作业,DUEDATE_等于当前时间加3天,HANDLER_TYPE_指向定时器处理器。JobExecutor周期扫描筛出到期作业,加锁、执行定时器逻辑,再删除作业记录。

这就是为什么“用户任务还在,但流程其实已经有一条定时器在倒计时”。只看任务表的话,完全看不到这条隐藏的推进线索,而它恰恰是很多“流程莫名自动走了”现象的数据来源。

5.3 事件订阅表:等待外部信号的登记簿

ACT_RU_EVENT_SUBSCR记录的是等待中的中间捕获事件,比如信号捕获、消息捕获。字段EVENT_TYPE_区分signal还是message,EVENT_NAME_是订阅的事件名,EXECUTION_ID_指向等待中的执行实例。外部系统调用SignalEventReceivedBuilder时,引擎就是拿事件名去匹配订阅表,找到对应执行实例再继续推进。

线上排查“流程等不到消息”这类问题,核心就是查订阅表有没有记录,以及事件名是否完全一致。Event name的大小写、是否带了不必要的空格,都可能导致匹配不上,这些细节在订阅表里一眼就能看出来。

6. 运行时数据的生命周期:什么时候清、什么时候积压

运行时表的设计目标就是“数据只在需要的时候存在”。流程正常结束时,执行实例、任务、变量、事件订阅、作业都会被清掉,历史数据才会持久化到ACT_HI_表。

6.1 流程结束后表里该剩什么:健康检查法

一个没有长期存活流程的系统,ACT_RU_TASK、ACT_RU_EXECUTION里的行数应该很低。如果数量持续上涨且不是业务需要,基本就是流程没走完或没人处理。我习惯给系统加一个巡检SQL,每天看一眼:

SELECT COUNT(*) AS exec_cnt FROM ACT_RU_EXECUTION WHERE IS_ACTIVE_ = 1; SELECT COUNT(*) AS task_cnt FROM ACT_RU_TASK; SELECT COUNT(*) AS timer_cnt FROM ACT_RU_TIMER_JOB; SELECT COUNT(*) AS deadletter_cnt FROM ACT_RU_DEADLETTER_JOB;

这四个数字的变化能反映系统健康度。某个数字突然飙升,往往比任何监控告警都先暴露问题。

6.2 积压判据与索引建议

积压一般来自三种情况:等待人工任务的流程太多;定时器作业因为执行器问题积压;死信表堆积说明作业反复失败。运行时表变大后,查询性能下降明显,我建议至少给这几列加索引:ACT_RU_EXECUTION的PROC_INST_ID_、PARENT_ID_,ACT_RU_TASK的PROC_INST_ID_、ASSIGNEE_,ACT_RU_VARIABLE的EXECUTION_ID_、TASK_ID_,作业表的DUEDATE_。这些索引都能直接支撑最常见的问题排查SQL。

6.3 正确清理姿势:别直接动数据库

不要直接DELETE数据库表。对已经无用的进行中流程,应该用runtimeService.deleteProcessInstance传入删除原因;对外部原因长期挂起、无法推进的流程,先挂起、再排查、再删除。历史表有独立的历史清理机制,可以按配置定期自动清理。虽然运行时表相对好清理,但直接动库永远是最后的兜底手段,一旦删错关联数据,流程实例会变成孤儿数据,后续恢复成本极高。

7. 从运行时表反查三组线上故障:完整排查链路

最后分享三组我在真实环境里排查过的故障,把前面所有知识点串起来看。

7.1 案例一:任务显示完成但流程停着

现象:用户任务已完成,但流程实例状态一直不结束。排查链路:先查ACT_RU_EXECUTION中该流程所有活跃分支,发现除了用户任务,还有一条子执行处于等待定时器边界事件的状态。也就是说,流程并不是坏了,而是在等一个排定的定时器。这类问题的根因往往是流程设计上确实有分支在等待,只是前端没展示出来。看表和看流程图要互相印证,不能只凭任务表下结论。

7.2 案例二:定时器到点但作业不执行

现象:定时器到时间后没有任何反应。我查ACT_RU_TIMER_JOB发现DUEDATE_早已过去,但LOCK_EXP_TIME_还在未来,说明作业被某个节点锁住了。进一步查看到该节点曾异常停机,锁没有及时释放。处理方式:确认该节点不再参与竞争后,手动把锁时间改到过去,让其他节点重新抢锁,然后观察作业是否被执行。

这里要特别提醒:修改锁字段属于非常规修复,操作前必须确认没有别的引擎节点正在执行同一作业,否则可能重复触发定时器逻辑。这个操作和线上改数据一样,需要先留痕、再执行、后验证。

7.3 案例三:流程实例删不掉,报外键约束错误

现象:删除流程实例时报外键约束,无法删除。原因通常是事件订阅表、作业表或身份关系表还有关联数据。我的处理过程:先把流程挂起,再查ACT_RU_EVENT_SUBSCR和作业表,确认是否存在等待信号或未执行的定时器,处理完之后再调用删除接口。删除失败的本质是运行时表里残留了和该实例关联的“旁路数据”,把这些旁路数据理清,删除就顺利了。

8. 写在最后:用一次边跑边查的方式建立表直觉

运行时数据表是理解Activiti的一把钥匙。我自己的学习方式很土:准备一个包含并行网关、子流程、定时器边界事件、消息中间捕获事件的Demo流程,每操作一步就SELECT * FROM各张表,把插入、更新、删除的时间点和字段变化记录下来。这样过一遍之后,再看任何线上问题都不慌,因为数据会告诉你引擎正在干什么、卡在哪、为什么卡。

建议你按这个思路把第一批SQL亲手跑一遍。先在本地起一个最小流程,走一步查一次,把每张表的变化和BPMN图的节点对应起来。这个习惯养成之后,再看ACT_RU_开头这批表,就像看自己写的业务表一样亲切。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询