Activiti学习系列写到第31篇这次专门把运行时数据表拿出来讲。原因很简单我见过太多人API调得飞起一遇到流程卡住、待办丢失、定时任务不触发就完全没方向而这些线上问题最后都会落到以ACT_RU_开头的那几张表上。如果你也想真正把Activiti学透运行时数据表是你绕不过去的一关。Activiti的数据表有很多但按职责分其实很清晰RE表存流程定义这类静态仓库数据RU表存正在执行的流程状态HI表存已发生的历史轨迹ID表存用户体系GE表存字节数组和全局属性。这里面的RU表也就是Runtime运行时表是今天的主角。它是流程引擎的当前时刻表反映的是此刻正在发生的一切。这篇文章会把它们的表结构、字段含义、表间关系、常用SQL、清理策略和排查经验一次讲清楚适合刚开始学Activiti的初学者也适合正在为线上数据头疼的开发者。1. 运行时数据表在Activiti里的定位1.1 从表前缀看懂Activiti的整体设计Activiti第一次建库的时候会生成五十多张表第一次见到的人基本都会头皮发麻。但只要记前缀思路就通了一半。以ACT_GE_开头的是通用表GE是General的缩写。这里面最常打交道的ACT_GE_BYTEARRAY用来存二进制内容流程图的PNG、流程定义的XML、序列化后的变量对象都放在这里。ACT_GE_PROPERTY则存引擎级别的属性比如数据库版本号、历史级别配置每次引擎启动都会检查。以ACT_RE_开头的是仓库表RE是Repository。ACT_RE_DEPLOYMENT记录部署包ACT_RE_PROCDEF记录流程定义。你部署一次流程这两张表会各多一条数据而且流程定义还有版本号字段同一个Key的流程部署多次会产生多个版本。以ACT_RU_开头的是运行时表RU是Runtime这是文章的主角。以ACT_HI_开头的是历史表HI是History流程跑完之后数据会在这里形成审计轨迹比如ACT_HI_PROCINST是流程实例历史、ACT_HI_TASKINST是任务历史、ACT_HI_ACTINST是节点执行历史。历史表里的记录默认不会被引擎自动删除体量会一直增长。以ACT_ID_开头的是身份表存用户和组现在大部分项目用自己的用户体系这些表一般用不上。把这五组表的关系概括一下就是RE表告诉引擎流程长什么样RU表告诉引擎流程现在跑到哪HI表告诉引擎流程以前跑过什么GE表给其他表打杂ID表是引擎自带的通讯录。运行时表之所以重要是因为所有正在发生的状态都在它身上而流程引擎的核心工作其实就是不断读它、改它、写它。1.2 为什么要单独维护一套运行时表有人可能会想既然历史表记录了所有轨迹为什么还要另搞一套运行时表从历史记录里倒推当前状态不就行了答案是不行。Activiti本质上是一个状态机它需要在任意时刻极快地回答两个问题当前流程实例停在哪个节点接下来能触发什么操作如果每次都要从历史表全量扫描再倒推性能会差到没法用。这就好比查现在几点钟你不会去翻一遍过去每一秒的监控录像看一块当前时刻表才是正经做法。运行时表在语义上是可变状态历史表在语义上是事实记录。两者混在一起会带来很麻烦的问题一个流程实例在多个并行分支下会有多个执行对象任务和变量在运行中被反复增删改这些数据如果和历史记录塞在同一张表里表结构会异常复杂事务边界也难以控制。把可变状态拆出来单独立表是领域建模里一个很常规也很实用的思路。还有一个实际层面的原因清理。流程结束、实例删除这种操作只需要清理RU表里对应的记录就行历史表完全不受影响。如果混在一张表里删除操作就会误伤审计数据。所以运行时表的临时性其实是有意设计的它不是缺点而是特性。1.3 运行时表家族的完整清单以Activiti 5.21之后的主流版本为例运行时表大概长这样表名职责典型应用场景ACT_RU_EXECUTION流程实例和执行分支查流程实例状态、当前节点ACT_RU_TASK待办任务待办列表、任务签收ACT_RU_VARIABLE流程变量查变量、传参、做条件判断ACT_RU_ACTINST正在活跃的节点实例流程监控图高亮当前节点ACT_RU_IDENTITYLINK任务和流程的参与者关系候选人查询、任务汇总ACT_RU_JOB事务作业异步任务、消息事件ACT_RU_TIMER_JOB定时作业定时边界事件、定时开始事件ACT_RU_SUSPENDED_JOB被挂起作业流程挂起时的定时器ACT_RU_DEADLETTER_JOB死信作业重试失败后的作业ACT_RU_EVENT_SUBSCR事件订阅信号、消息事件的广播订阅需要说明版本差异。在Activiti 5.21之前定时器和异步执行都混在ACT_RU_JOB一张表里用TYPE_字段区分所以老项目的排查思路和这里讲的会有些出入。如果你的项目用的是5.21之后的版本或者基于它衍生的6.x、7.x版本就是上面这个表。我建议拿到一个新环境的Activiti时第一步不要急着写代码而是把这几张表的结构打印出来字段、索引、外键全部过一遍。我经常说Activiti的排查能力不是从API文档里来的是从表结构里来的。2. 核心运行时表逐一拆解2.1 ACT_RU_EXECUTION——流程实例和执行分支的主控台这张表是整个运行时数据里最核心的。它有两个身份一是存流程实例本身二是存流程实例下的执行分支。这两个身份经常把初学者绕晕。流程实例启动时引擎会创建一个根执行Root Execution根执行的ID_和PROC_INST_ID_相同流程实例ID其实就是根执行ID。但是遇到并行网关、子流程调用这种场景引擎会在这个根执行下派生出多个子执行这些子执行也各占一行它们的ID_是独立的而PROC_INST_ID_仍然指向根执行。所以一张流程图里到底有几条执行线在并发地跑看的是这张表有多少行而不是有多少个流程实例。关键字段里我最常用的是这么几个ID_执行对象ID子执行的ID和流程实例ID不同这是正常的PROC_INST_ID_流程实例ID根执行下ID_与它相同ROOT_PROC_INST_ID_根流程实例ID跨子流程追踪时用PARENT_ID_父执行ID子执行指回根执行SUPER_EXEC_子流程里指回外层执行调用子流程时用ACT_ID_当前停留在哪个活动节点排查卡点最有用BUSINESS_KEY_业务主键一般存业务单据号SUSPENSION_STATE_1代表激活2代表挂起值得注意的还有一个REV_字段它是乐观锁版本号。每次更新记录时引擎会比对REV_并发情况下防止两个线程同时改同一条执行数据。如果你在日志里看到couldnt update execution这类报错基本就是乐观锁冲突说明同一个流程实例被并发操作了。我之前在某个项目里排查过一个怪现象一个流程实例在ACT_RU_EXECUTION里占了五六行。当时同事以为是脏数据差点直接删。实际上是因为流程里用了并行网关每个分支都有独立的子执行数据是完全正常的。所以看到多行记录先别动手删先看PARENT_ID_和IS_CONCURRENT_字段确认是不是并行分支。2.2 ACT_RU_TASK——待办任务的大本营用户任务是Activiti里最常见的节点ACT_RU_TASK就是当前所有未完成用户任务的集合。它的EXECUTION_ID_指向ACT_RU_EXECUTION说明这个任务是挂在哪个执行分支下的。这张表是业务系统查询最频繁的表待办列表、我的工单、审批汇总基本都从这查。关键字段ID_任务IDNAME_任务名称ASSIGNEE_当前办理人OWNER_任务的属主一般记录任务创建人PROC_INST_ID_所属流程实例EXECUTION_ID_所属执行TASK_DEF_KEY_流程定义里对应的任务节点Key查看是哪个模型节点时很有用CREATE_TIME_任务创建时间CLAIM_TIME_签收时间区分签收模式和直接分配模式DUE_DATE_到期时间催办超时提醒靠它DELEGATION_委派状态委派出去时是PENDINGSUSPENSION_STATE_任务挂起状态有一个点经常被忽略任务的ASSIGNEE_并不等于唯一的办理方式。如果任务是通过候选人方式分配的ASSIGNEE_是空的ACT_RU_IDENTITYLINK表里会存候选人和候选组。所以统计一个人名下的待办不能只查ASSIGNEE_还要把候选人关系表带上。还有一个关于委派和转办的点。转办是换个办理人ASSIGNEE_直接改委派是临时让助理代办ASSIGNEE_还是原来的办理人DELEGATION_标记成PENDING。如果你没留意DELEGATION_统计任务的时候就会把委派中的任务漏掉或者算错人。2.3 ACT_RU_VARIABLE——流程变量的临时仓库流程变量是Activiti在不同节点之间传递业务数据的核心方式ACT_RU_VARIABLE存的就是流程变量在当前运行期的快照。这张表的字段设计很有意思它没有为每种变量类型单独建列而是用TYPE_字段标记类型再让不同类型的数据落到不同的存储列里。核心字段TYPE_变量类型常见有string、long、double、boolean、serializable、bytesLONG_Long类型变量的值DOUBLE_Double类型变量的值TEXT_String类型短文本TEXT2_超长String文本BYTEARRAY_ID_序列化对象或字节数据指向ACT_GE_BYTEARRAY表PROC_INST_ID_流程级变量所属流程EXECUTION_ID_执行级变量所属执行TASK_ID_任务级局部变量所属任务NAME_变量名这种设计是空间换灵活性的典型。如果为每种类型都建一列单独存储表会有大量空列基本类型和复杂类型的处理逻辑也要在数据库层做区分。用TYPE_加多列的方案引擎读取的时候按类型找对应列逻辑统一存储也紧凑。变量作用域是特别值得说的点。流程级变量挂在PROC_INST_ID_上所有人共享任务级局部变量挂在TASK_ID_上只有当前任务能看到任务一完成这几个变量就被清掉。我在项目里遇到过好几次变量明明设了下一步就是取不到的诡异问题最后查出来都是作用域填错了把任务局部变量当流程变量用了。还有一个高频坑同一个执行范围内变量名不能重复。如果你在服务任务里通过runtimeService.setVariable写入了一个和入口参数同名的变量引擎会直接报变量已存在之类的错误。所以命名变量的时候一定要有全局统一的前缀习惯别随手起a、b、c这种名字后面查问题想哭都来不及。2.4 ACT_RU_ACTINST——正在运行的节点快照ACT_RU_ACTINST对应的是当前正在活跃的节点实例。节点指的是BPMN模型里的活动元素用户任务、服务任务、接收任务、网关这些都算。节点一旦进入引擎就往这里插一条记录节点完成时记录被更新或移除语义就是这个节点现在正活着。在Activiti 5的某些版本里ACT_RU_ACTINST的记录在节点结束后并不立即删除而是更新END_TIME_之后一直留到流程实例结束才清理也有版本是直接删除同时往历史表写ACT_HI_ACTINST。所以写代码的时候不要依赖ACT_RU_ACTINST里一定只有当前活跃节点这种假设不同版本行为有差异。最稳的做法是查END_TIME_为空的记录把它当成当前活跃节点。字段里面值得关注的有ACT_ID_活动节点Key对应流程图里的节点标识ACT_NAME_节点名称ACT_TYPE_节点类型比如userTask、serviceTaskASSIGNEE_当前节点的办理人PROC_INST_ID_所属流程实例EXECUTION_ID_所属执行START_TIME_节点进入时间END_TIME_节点结束时间为空代表还在执行这张表是做流程监控图最趁手的数据源。很多项目会做一个当前流程走到哪一步的展示后端的核心SQL就是从这张表把未结束的节点记录查出来再配上流程图坐标去高亮。需要提醒的是ACT_RU_ACTINST在并行分支下会有多条活跃记录这是引擎的正常行为。你画监控图的时候要按PROC_INST_ID_聚合把多个活跃节点同时展示出来而不是只取一条否则并行流程的展示就会失真。2.5 ACT_RU_IDENTITYLINK——参与者关系簿这张表存的是谁和哪个流程或任务有关系理解成一张关系表就行。任务维度上用户任务配置了候选人、候选组、办理人、属主这些关系都会落到这里。TYPE_字段常见的值有candidate、assignee、owner、starter。比如一个用户任务配了三个候选人任务表里一行任务而身份关系表里会多出三行candidate记录。流程维度上流程发起人一般会有一条starter记录在做流程发起人维度统计时有用。子流程和业务关联的参与者也会在这里体现。这张表最典型的用途是按人查待办。如果你的项目走了候选人分配模式那么ASSIGNEE_为空想找出某个人能处理哪些任务就必须来ACT_RU_IDENTITYLINK里按USER_ID_和TYPE_candidate查TASK_ID_再回到ACT_RU_TASK拿任务详情。要注意区分ACT_RU_IDENTITYLINK和ACT_ID_表。前者是动态的运行时关系记录的是某一次流程执行里的任务分配后者是静态的用户体系数据存的是组织架构里的用户和组。很多初学者在这里搞混写SQL的时候join错表结果数据对不上。2.6 作业四表JOB/TIMER_JOB/SUSPENDED_JOB/DEADLETTER_JOBActiviti的异步执行器是用来处理定时任务和异步延续的。5.21版本以后作业从一张表拆成了四张这四张表的分工非常像消息中间件的延迟队列设计理解起来不难。ACT_RU_JOB是正在被异步执行器处理的事务作业包括异步服务任务、消息事件这些。ACT_RU_TIMER_JOB存定时器作业比如定时边界事件、定时开始事件、Timer中间捕获事件。ACT_RU_SUSPENDED_JOB存挂起流程对应的作业流程被挂起后原本该执行的定时器会被搬到这里挂起解除后再搬回去。ACT_RU_DEADLETTER_JOB是死信作业作业执行失败且重试次数用完了就会被扔到这里方便人工介入排查。作业表的关键字段DUEDATE_作业到期时间定时器什么时候触发看它RETRIES_剩余重试次数每次失败会扣减LOCK_OWNER_当前锁住这个作业的执行器实例LOCK_EXP_TIME_锁的过期时间EXCLUSIVE_是否独占执行线上排查定时任务不触发顺序基本是固定的先看ACT_RU_TIMER_JOB里这条作业的DUEDATE_过了没有过了就看是不是被LOCK_OWNER_锁住且锁没过期再看RETRIES_是不是扣到0进了死信表。这三步能解决九成以上的定时任务问题。2.7 ACT_RU_EVENT_SUBSCR——事件订阅表这张表存的是流程运行过程中注册的事件订阅关系。比如一个流程通过信号事件等待另一个流程发信号引擎就会在这里记录一条订阅。当sendSignal被执行时引擎通过这张表找到所有在等这个信号的执行把信号推过去。它最常见的应用场景就是跨流程实例的协调一条流程跑到中间节点停下等外部系统通过API发一个信号收到信号后流程才继续往下走。如果突然发现流程停着但事件触发不了除了看订阅是否被删还要检查CONFIGURATION_字段和流程实例ID能不能对得上。这张表的记录数和业务复杂度直接相关在纯顺序流转的简单流程里它可能一直为空。所以很多人学了几年Activiti都没碰过它这很正常。但只要你开始接触信号、消息这些跨流程能力它就变得关键了。3. 运行时表之间的协同一次请假流程的数据流转全程光看单张表很容易记混把一次完整的流程生命周期拆开看所有表的协作关系就清晰了。这里用最简单的请假流程举例开始节点然后部门审批再人事确认最后结束。3.1 发起流程时数据如何落库调用runtimeService.startProcessInstanceByKey(leave)之后引擎在一个事务里做了以下几件事在ACT_RU_EXECUTION里插入一行ID_和PROC_INST_ID_相同BUSINESS_KEY_存入业务单号ACT_ID_指向当前节点。在ACT_RU_ACTINST里插入一行对应正在活跃的部门审批节点。在ACT_RU_TASK里插入一行待办任务办理人指向部门主管CREATE_TIME_写入当前时间。如果启动时带了流程变量在ACT_RU_VARIABLE里按流程级作用域插入记录。在ACT_RU_IDENTITYLINK里写入发起人关系TYPE_一般是starter。这样一次简单的发起流程操作在数据库层面落了好几张表而且全部在同一个事务里。所以如果你在日志里看到发起流程偶尔失败去查数据库多半能发现某个表的数据没写完整而事务回滚后所有表都会保持原样这就是强一致性的价值。3.2 任务办理时数据如何变化部门主管在系统里点同意前端调用taskService.complete(taskId, variables)。引擎在一个事务里做这些事删除ACT_RU_TASK里这条任务记录同时在ACT_HI_TASKINST里生成完成态的历史任务记录END_TIME_写入当前时间。更新ACT_RU_ACTINST里当前节点记录标记节点已结束同时往ACT_HI_ACTINST里写入节点历史。更新ACT_RU_EXECUTION的执行位置ACT_ID_从部门审批变成人事确认。如果complete方法带了变量在ACT_RU_VARIABLE里按传入参数更新或新增。下一个人事确认节点生成新任务在ACT_RU_TASK里插入新记录。如果你开着SQL日志看这个操作会发现是一连串INSERT、DELETE、UPDATE交织执行。我强烈建议初学者在测试环境这么干一次把日志打开对着表一条一条看比看十篇文章都管用。3.3 流程结束时数据如何清场人事确认完成后流程进入结束事件。引擎会做最后的清理把ACT_RU_EXECUTION里对应的根执行和子执行记录删除清理ACT_RU_TASK、ACT_RU_VARIABLE、ACT_RU_ACTINST、ACT_RU_IDENTITYLINK里关联的记录再往ACT_HI_PROCINST里写入流程实例历史记录END_TIME_和DURATION_补齐。也就是说一个正常走完的流程运行时表里不应该留下任何痕迹。记住这个判断标准非常重要如果一条流程已经走完但RU表里还有它的数据那就是有异常。最常见的是并行分支里某条分支没汇聚成功导致子执行和子任务挂在那没被清理。3.4 顺着执行表查遍所有运行时数据运行时表之间的关系用一句话概括ACT_RU_EXECUTION是树干ACT_RU_ACTINST和ACT_RU_TASK是枝条ACT_RU_VARIABLE和ACT_RU_IDENTITYLINK是叶子。遇到问题先找到树干再顺藤摸瓜。具体来说一个流程实例卡住了先用PROC_INST_ID_去ACT_RU_EXECUTION查能看到当前执行状态、BUSINESS_KEY_、SUSPENSION_STATE_这些基础信息再去ACT_RU_ACTINST看当前活跃节点是哪个再去ACT_RU_TASK看有没有对应的待办任务变量和参与者关系按需去查。把这条路径背下来线上排查就已经赢了一半。跨表查询的时候要注意一个细节ACT_RU_TASK和ACT_RU_VARIABLE都有PROC_INST_ID_但它们的粒度不同。变量表可以是流程级、执行级、任务级三个维度只看PROC_INST_ID_可能会漏掉执行级和任务级的数据。要拿到全量变量最好联表把EXECUTION_ID_和TASK_ID_相关的记录也查出来。4. 实战经验查询、索引与数据治理4.1 线上高频SQL模板下面这几条SQL是我在项目里反复用的直接改参数就能用。按办理人查待办任务顺便带出业务单号SELECT t.ID_, t.NAME_, t.CREATE_TIME_, t.DUE_DATE_, p.BUSINESS_KEY_ FROM ACT_RU_TASK t LEFT JOIN ACT_RU_EXECUTION p ON p.PROC_INST_ID_ t.PROC_INST_ID_ AND p.PARENT_ID_ IS NULL WHERE t.ASSIGNEE_ #{userId} AND t.SUSPENSION_STATE_ 1 ORDER BY t.CREATE_TIME_ DESC;按候选人查待办任务SELECT DISTINCT t.ID_, t.NAME_, t.CREATE_TIME_ FROM ACT_RU_TASK t INNER JOIN ACT_RU_IDENTITYLINK l ON l.TASK_ID_ t.ID_ AND l.TYPE_ candidate WHERE l.USER_ID_ #{userId} AND t.SUSPENSION_STATE_ 1;查流程实例当前活跃节点SELECT ai.ACT_ID_, ai.ACT_NAME_, ai.ACT_TYPE_, ai.ASSIGNEE_, ai.START_TIME_ FROM ACT_RU_ACTINST ai WHERE ai.PROC_INST_ID_ #{procInstId} AND ai.END_TIME_ IS NULL ORDER BY ai.START_TIME_;查即将到期的定时任务SELECT * FROM ACT_RU_TIMER_JOB WHERE DUEDATE_ BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 5 MINUTE);查可能卡住的作业SELECT * FROM ACT_RU_DEADLETTER_JOB; SELECT * FROM ACT_RU_JOB WHERE LOCK_EXP_TIME_ NOW();这些SQL在排查问题时极其好用建议直接存成一个排查脚本文件遇到线上问题跑一遍基本就有方向了。4.2 索引应该怎么加Activiti创建表的时候自带了一些基础索引但真实项目里查询条件五花八门默认索引经常会不够用。我建议按实际查询模式补索引重点检查这几个组合ACT_RU_TASK(ASSIGNEE_, SUSPENSION_STATE_)待办查询必用ACT_RU_TASK(PROC_INST_ID_)按流程查任务ACT_RU_VARIABLE(PROC_INST_ID_, NAME_)按流程查变量ACT_RU_VARIABLE(EXECUTION_ID_, NAME_)按执行查变量ACT_RU_EXECUTION(PROC_INST_ID_, PARENT_ID_)查子执行ACT_RU_ACTINST(PROC_INST_ID_, END_TIME_)查活跃节点ACT_RU_IDENTITYLINK(TASK_ID_, TYPE_)候选人关联查询ACT_RU_TIMER_JOB(DUEDATE_, LOCK_EXP_TIME_)定时任务扫描加索引的时候留个心眼有的Activiti版本升级会重建或修改索引名如果和你手工建的索引重名了会有冲突。加索引前先看一下当前版本的建表脚本里已经有哪些索引避免重复。另外RU表的写频率很高索引太多会影响写入性能所以索引要根据真实慢查询来加别一上来全加满。4.3 运行时数据的清理与归档按理说流程结束后运行时数据会被清空但真实项目跑了半年以后RU表越积越大几乎是必然的。原因无非这么几种流程真的没走完、网关设计失误导致分支悬空、有人手动删过任务导致引擎状态错乱、旧流程定义还在跑但没人处理。清理运行时数据最安全的方式是走官方API。比如清理某个异常流程实例先用runtimeService.deleteProcessInstance(procInstId, deleteReason)删流程实例它会递归清理关联的RU表数据。如果有子流程得先把子流程删掉再删父流程。直接写DELETE语句删表是最不建议的操作引擎的Session缓存和数据库不一致了后续什么奇怪问题都会冒出来。如果需要归档我的做法是把ACT_HI_相关的历史记录定时导出到数仓同时按天清理已结束流程在RU表里的残留数据。前提是确认这些残留数据的流程确实已经不在运行中判断标准就是ACT_RU_EXECUTION里查不到对应的PROC_INST_ID_。清理之前先备份这句话我说多少遍都不嫌多。4.4 监控与巡检运行时表就像流程引擎的仪表盘建议做一个每日巡检脚本主要看四件事ACT_RU_TASK的总量变化突增通常意味着有流程堆积ACT_RU_EXECUTION按流程定义分组的数量找出占比最高的流程定义ACT_RU_DEADLETTER_JOB里有几条死信死信多说明有作业一直失败ACT_RU_JOB里锁过期的记录有锁过期说明执行器可能有问题巡检不是等出事了才跑而是每天固定跑一遍把数据存下来画趋势图。我见过的大多数线上事故其实在趋势图里提前两三天就有信号了。5. 高频问题与排查实录5.1 待办任务越积越多怎么查现象ACT_RU_TASK表体量暴涨待办查询越来越慢。排查思路按流程定义分组统计未完成任务数量定位占比最大的流程。看这些任务的CREATE_TIME_分布判断是持续增长还是某一天突然爆发。检查对应流程定义的网关设计尤其是并行网关是否有对应的汇聚网关。并行分支条件永远满足不了的时候任务会永远挂着。检查流程定义是不是升级了版本旧版本实例还在跑但已经没人按新表单去处理了。这类问题定位以后清理动作一定先走API不要直接动表。我见过最严重的一次就是因为有人从数据库里强行删了几条任务记录结果引擎缓存的执行状态还在流程实例直接变成僵尸数据后续所有操作都报错最后只能从历史表重建流程。5.2 定时任务不触发怎么查现象定时边界事件到了时间流程没有往下走。排查顺序查ACT_RU_TIMER_JOB里对应作业的DUEDATE_。如果时间早就过了还没执行继续看LOCK_OWNER_和LOCK_EXP_TIME_判断作业是不是被某个执行器锁住了且没释放。看RETRIES_。如果扣到0去ACT_RU_DEADLETTER_JOB里找这条作业看异常堆栈信息。检查异步执行器的配置。AsyncExecutor是否开启、核心线程数、最大线程数、锁存活时间这些参数都有可能影响。检查数据库和应用服务器的时区是否一致。这个坑很隐蔽时区差8小时定时任务就可能提前或者延后8小时触发。如果上面都查不出问题记得看一眼ACT_RU_SUSPENDED_JOB。流程实例被挂起后作业会被搬到这里恢复流程时会再搬回TIMER_JOB。搬移过程偶尔会出问题作业就卡在SUSPENDED_JOB里不动弹。5.3 流程实例删不掉现象调用deleteProcessInstance报错提示子流程未清理或者外键约束失败。排查思路在ACT_RU_EXECUTION里查PROC_INST_ID_对应的所有记录看有没有子执行特别是有没有SUPER_EXEC_引用了外层流程。如果有子流程先递归删除所有子流程实例再删父流程实例。如果报的是历史表外键问题检查历史级别配置或者先把归档数据处理好再删。这个问题通常在设计阶段就能规避。流程里只要用了子流程就要明确删除策略是级联删除还是先终止子流程再删父流程。我后来在项目里专门写了一个递归删除工具方法把子流程实例全部捞出来按从内到外的顺序删之后这个问题就再没出现过。5.4 变量表膨胀导致查询慢现象ACT_RU_VARIABLE表特别大按流程实例查变量越来越慢。原因不外乎三个流程里存了大量的大对象变量比如byte数组、序列化后的自定义实体。没有给PROC_INST_ID_或EXECUTION_ID_加索引。循环节点里反复创建任务级局部变量数据量被放大。改进方向能用基本类型就别存大对象。流程变量里只放业务必要参数大对象放到专门的业务表流程变量里只存业务表ID。索引要补上尤其是按流程实例查变量的场景。定期清理已结束流程对应的运行时变量。正常情况下流程结束会自动删但残留数据多的项目要专门做清理任务。5.5 手动改表数据的教训最后一个专门拿出来说因为它太容易发生了。线上有个流程卡住了任务没人处理有些人第一反应就是写一条DELETE FROM ACT_RU_TASK WHERE ID_ xxx。这个操作看起来很直接实际上是在引爆一颗雷。Activiti引擎在运行时有自己的Session缓存和内存状态。你手动把数据库里某条任务删了引擎内存里还认为这条任务存在于是会出现任务状态和数据库状态不一致。接下来不管你是complete还是delete引擎都会拿内存状态去和数据库比对乐观锁版本号一对不上就开始报错。更严重的是如果这条任务关联的流程实例还有其他子执行清理链条断了整棵执行树都会变成僵尸数据。正确做法永远是走API挂起、终止、删除实例都有官方方法。哪怕官方方法无法完全处理也应该先分析原因从流程设计上调整而不是绕过引擎直捣数据库。这是我做Activiti相关项目以来最深刻的一条教训。我个人学Activiti最大的感受是真正让我开窍的不是学会画流程图而是把数据库表结构彻底过了一遍。后来所有线上问题无论是前端报错还是引擎异常最终几乎都能落到某张RU表的数据上。所以如果你也在学Activiti我建议别只顾着调API花一个下午把ACT_RU_这几张表的结构、字段、外键关系打印出来对着看之后再遇到问题会少走很多弯路。最后再分享一个小习惯我在测试环境里会一直开着SQL日志然后故意跑一遍发起流程—办理任务—结束流程把整个过程产生的SQL全部捞出来对着表看数据的增删改。这个过程重复几次之后你基本就能在脑子里实时看见流程数据在表之间流转的样子。这个功夫比硬记再多API都有用。