资讯详情

Activiti ACT_RU_JOB表深度拆解:定时器、异步作业与运维排查实战

📅 2026/10/17 3:26:48 | 华诺云谱 👁 阅读
Activiti ACT_RU_JOB表深度拆解:定时器、异步作业与运维排查实战
做流程平台的人迟早要跟Activiti那几十张数据库表打交道。跑通一个“开始→人工审批→结束”的流程并不难真正让你头疼的往往是那些“该动却不动”的东西——定时器没触发、异步消息没消费、作业在表里堆了一堆。这些问题绕来绕去最后都指向同一张运行实例表ACT_RU_JOB。从这篇开始我打算把Activiti的25张表按经典分类挨个拆开讲第一篇就落在“运行实例表14”这张ACT_RU_JOB上。别被编号绕住它其实就是一张“作业调度表”专门存放定时器、异步消息、临时触发等需要引擎后台处理的任务。你现在遇到的大多数作业执行性问题答案都藏在这张表里。这篇文章适合三类人看用Activiti开发流程平台的工程师在生产环境维护流程引擎的运维以及正在做流程引擎技术选型、想摸清Activiti持久层设计的人。读完你会清楚每个字段的真实含义、作业是怎么被调度起来的、以及作业卡死该从哪下手排查。1. ACT_RU_JOB 在25张表里到底是一个什么位置1.1 经典25张表的五组划分Activiti 5.x时代大家习惯把数据库表按用途分成五组这和表前缀是对应的。分组表前缀典型用途代表表通用表ACT_GE_引擎级公共数据ACT_GE_PROPERTY、ACT_GE_BYTEARRAY仓库表ACT_RE_部署包、流程定义、模型ACT_RE_DEPLOYMENT、ACT_RE_PROCDEF运行时表ACT_RU_流程执行中的动态数据ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_JOB历史表ACT_HI_流程实例、活动、任务的历史归档ACT_HI_PROCINST、ACT_HI_TASKINST身份表ACT_ID_用户、组、成员关系ACT_ID_USER、ACT_ID_GROUP运行时表这一组是最容易让人迷惑的因为每张表都带着“ACT_RU_”前缀名字又特别接近。ACT_RU_EXECUTION管执行流ACT_RU_TASK管任务ACT_RU_VARIABLE管变量而ACT_RU_JOB管的则是“需要后台调度的工作项”。1.2 ACT_RU_JOB不是“任务表”而是“作业调度表”先说一个最常见的误区有人看到ACT_RU_JOB里的“JOB”就以为它等于待办任务其实不对。ACT_RU_TASK才是人工任务/服务任务对应的表ACT_RU_JOB的核心是异步与定时调度。比如流程启动事件配置了定时器到期后需要启动流程流程中有一个定时边界事件比如“超过72小时未审批自动驳回”引擎配置了异步执行器需要把业务逻辑丢到后台线程池执行。这些场景都会在ACT_RU_JOB里生成记录。换句话说这张表是流程引擎异步机制的“待办队列”引擎的调度线程会不断扫描它把到期、可执行、未被锁定的作业捞出来处理。理解了这一点再看它的字段结构就顺了。2. 表结构逐字段拆解ACT_RU_JOB的“五脏六腑”2.1 主键、乐观锁、作业类型经典版本里ACT_RU_JOB的表结构大致长这样以MySQL为例CREATE TABLE ACT_RU_JOB ( ID_ varchar(64) NOT NULL, REV_ integer, TYPE_ varchar(255) NOT NULL, LOCK_EXP_TIME_ timestamp NULL, LOCK_OWNER_ varchar(255), EXCLUSIVE_ boolean, EXECUTION_ID_ varchar(64), PROCESS_INSTANCE_ID_ varchar(64), PROCESS_DEF_ID_ varchar(64), RETRIES_ integer, EXCEPTION_STACK_ID_ varchar(64), EXCEPTION_MSG_ varchar(4000), DUEDATE_ timestamp NULL, REPEAT_ varchar(255), HANDLER_TYPE_ varchar(255), HANDLER_CFG_ varchar(255), TENANT_ID_ varchar(255), PRIMARY KEY (ID_) ) ENGINEInnoDB DEFAULT CHARSETutf8;先说最基础的三个字段。ID_是主键通常由引擎生成全局唯一。没什么好说的但要注意它一般是字符串类型不是自增数字所以如果你在写监控脚本别按数值去排序。REV_是乐观锁版本号。引擎在更新一行时会先比对读取时的REV_和当前库里的REV_是否一致不一致就说明有其他事务改过了本次更新会失败。这种机制在多线程、多节点环境下特别重要避免两个执行器同时修改同一条作业记录。TYPE_是作业类型经典版本里常见的有timer和message。这一个字段决定了后面HANDLER_TYPE_、HANDLER_CFG_该用什么逻辑去解析。在Activiti 6之后作业被拆到多张表里TYPE_的体现方式也变了后面第6节我会专门讲。2.2 锁机制字段并发安全的关键LOCK_EXP_TIME_和LOCK_OWNER_是集群环境下最值得关注的两个字段。LOCK_OWNER_记录当前“锁住”这条作业的节点标识。在集群部署中多个流程引擎节点共用同一个数据库如果不加锁两个节点会同时把同一个定时器作业捞出来执行造成重复触发。这里的锁有点像一个会议室预约谁先登记了owner谁就有使用权其他人看到owner有名字就不碰。LOCK_EXP_TIME_是锁的过期时间。如果一个节点在执行作业时崩溃了它手里的锁不会主动释放如果这个过期时间设计得足够长其他节点只能傻等如果设计得太短又可能在作业还没执行完时就被别的节点抢走。引擎默认的锁等待时间一般较长具体值可以在异步执行器配置里调整。实际排查中我经常看到一种情况某个节点下线后ACT_RU_JOB里残留的LOCK_EXP_TIME_还是未来时间导致作业“看起来活着实际动不了”。这时候要么等锁过期要么手动把锁字段清掉。2.3 关联字段作业与流程上下文EXECUTION_ID_、PROCESS_INSTANCE_ID_、**PROCESS_DEF_ID_**三个字段用来把作业挂到对应的流程上下文上。PROCESS_INSTANCE_ID_指向流程实例EXECUTION_ID_指向具体的执行流PROCESS_DEF_ID_指向流程定义。为什么需要三列因为不是所有作业都挂在某个具体执行流下。比如“定时启动事件”在流程实例还没创建时就只有PROCESS_DEF_ID_和作业本身而“定时边界事件”一旦触发则必须知道是哪个执行流、哪个流程实例。这三个字段也是我们做关联查询的入口。排查问题时最常用的SQL就是拿PROCESS_INSTANCE_ID_去ACT_RU_JOB里查看看这个流程实例名下到底挂了几条未完成的作业。2.4 异常重试字段为什么作业会“死”掉RETRIES_是重试次数。作业执行抛出异常时引擎不会立刻放弃而是把RETRIES_减一等下一轮扫描再次尝试。默认通常是3次每次失败减1减到0之后这条作业就不再参与正常扫描了相当于进入“死亡状态”。EXCEPTION_MSG_记录最后一次异常的简短信息方便你一眼看出失败原因。EXCEPTION_STACK_ID_指向ACT_GE_BYTEARRAY表那里存着完整的异常堆栈。注意这个ID只是外键逻辑不一定有强约束所以即使堆栈内容被清理这里的ID也可能还留着历史值。实际开发中如果你发现某条作业一直不执行先查这几列。只要RETRIES_归零作业就跟“僵尸”一样躺在表里再也不会被调度。很多初学者不知道这一点以为是引擎卡住了其实只是重试次数被耗尽了。2.5 定时与循环字段DUEDATE_是作业的到期时间。引擎扫描时只会把当前时间已经超过DUEDATE_的作业捞出来执行。如果这个值是空通常表示“立刻执行”的异步作业。REPEAT_是重复执行规则。比如一段定时器事件配置了每小时执行一次作业执行完后引擎会根据REPEAT_重新计算下一次的DUEDATE_再插入一条新记录而不是让原记录死循环。这里有个容易踩的坑REPEAT_在不同版本里支持的格式不完全一致最常见的是ISO 8601重复周期如R3/PT10S表示重复3次、每次间隔10秒但某些版本也支持Cron表达式。做跨版本升级时这个字段一定要重点核对。2.6 处理器字段谁来执行这条作业HANDLER_TYPE_是作业处理器类型HANDLER_CFG_是给处理器用的配置参数。举例来说定时边界事件的HANDLER_TYPE_可能是timer-boundary-eventHANDLER_CFG_里存放的是对应的活动节点ID定时启动事件则是timer-start-eventHANDLER_CFG_里存的是流程定义的相关信息。引擎从表里取出作业后会根据HANDLER_TYPE_找到对应的JobHandler实现类再把HANDLER_CFG_传进去执行。这也意味着如果你自己扩展了作业处理器本质上就是在HANDLER_TYPE_和HANDLER_CFG_上做文章。2.7 建表DDL与索引建议上面已经给了完整DDL这里单独强调索引。很多生产环境里ACT_RU_JOB表数据量大、查询频繁如果不及时建索引执行器扫描作业会非常慢。经典版本一般会为以下字段建索引LOCK_EXP_TIME_扫描可获取作业时最常用的条件PROCESS_INSTANCE_ID_按流程实例排查作业DUEDATE_定时扫描时排序和过滤。我自己在维护环境里见过最惨的一次就是这表里积了上百万条死信作业而引擎每次扫描都要全表扫数据库CPU直接被打满。索引和表数据治理一样重要。3. 作业类型与Job Handler机制3.1 三类典型作业经典Activiti里作业类型主要分三类。第一类是Timer定时器作业。它来自流程定义里的定时事件包括定时启动事件、定时边界事件、定时中间事件。这类作业的DUEDATE_通常是一个未来的时间点到点触发。第二类是Message消息作业。当流程进入消息事件节点时引擎可能会生成一条消息作业等待外部系统把消息送到。它不像定时器那样固定到期更多是“挂起等待收到消息后触发”。第三类是异步执行作业。比如引擎配置了异步的service任务流程走到这个节点后不直接执行而是生成一个作业放到后台异步执行从而让流程线程快速返回。三种作业在ACT_RU_JOB里都靠TYPE_字段区分。在Activiti 6之后它们被拆到不同表里但是核心语义没有变。3.2 HandlerType与HandlerCfg怎么配合来看一张简化映射表HANDLER_TYPE_含义典型场景timer-start-event定时启动事件到时间自动发起新流程实例timer-boundary-event定时边界事件审批超时自动驳回/跳转timer-intermediate-transition中间定时事件流程暂停N小时后继续message-continuation消息事件继续异步等待消息后继续流程custom自定义处理器通过扩展实现特殊逻辑HANDLER_CFG_就是给这些处理器提供参数的比如活动ID、事件ID、流程定义ID等。如果你在代码里自定义过JobHandler会发现引擎并没有硬编码“哪条作业对应哪个类”而是通过HANDLER_TYPE_做映射。这也是ACT_RU_JOB设计上一个很优雅的点调度逻辑和业务逻辑解耦。4. 作业执行器从“排队”到“锁单”的全过程4.1 AsyncExecutor激活与参数首先要记住一句话不开启异步执行器ACT_RU_JOB里的定时器作业就是一张死表。Activiti默认情况下异步执行器未必是激活的。如果asyncExecutorActivate没有设置为true引擎根本不会启动后台扫描线程定时器到了时间也不会触发。在Spring Boot环境里你可以在配置里设置spring: activiti: async-executor-activate: true async-executor-core-pool-size: 8 async-executor-max-pool-size: 16 async-executor-queue-size: 100核心线程数、最大线程数、队列大小这些参数决定了引擎能同时跑多少作业。如果并发量大配置太小作业就会排队等待配置太大又会占满线程池影响其他服务。我在项目里常用的策略是先按数据库连接池的1/2到2/3来设置核心线程数再压测看是否存在作业积压。4.2 获取作业的查询逻辑异步执行器的工作流程本质上是“反复扫描、锁单、执行”。扫描时引擎会执行一个类似这样的获取查询SELECT * FROM ACT_RU_JOB WHERE RETRIES_ 0 AND (DUEDATE_ IS NULL OR DUEDATE_ now()) AND (LOCK_EXP_TIME_ IS NULL OR LOCK_EXP_TIME_ now()) ORDER BY DUEDATE_ LIMIT ?注意两个关键条件RETRIES_必须大于0锁必须已过期或尚未持有。所以只要重试次数归零作业就会被正常扫描直接跳过这也是前面说的“作业装死”的原因之一。4.3 锁定、执行、重试、释放引擎捞到作业后会先把LOCK_OWNER_写成当前执行器节点ID把LOCK_EXP_TIME_更新为一个未来的时间点再提交事务。这一步完成后其他节点再来扫描就会因为LOCK_EXP_TIME_还没过期而跳过这条作业。然后作业进入实际执行阶段。执行成功非重复作业会被直接删除重复作业则根据REPEAT_重新计算DUEDATE_并更新记录。执行失败RETRIES_减一EXCEPTION_MSG_和EXCEPTION_STACK_ID_会被记录下来。这里还有一个EXCLUSIVE_互斥位。如果EXCLUSIVE_为true表示同一个流程实例下的作业需要被同一个执行器顺序处理避免并发出问题。默认情况下大多数作业都是互斥的这保证了流程内部状态的一致性。4.4 重试、死信与异常熔断重试机制虽然好用但也很坑。比如某个定时任务依赖的外部接口一直超时每轮扫描都会把作业捞出来执行一次RETRIES_从3减到2再到1再到0每一次都在数据库里产生一条异常记录最后作业彻底停掉。这种设计本意是“容错”但不能替代业务层面的幂等和重试策略。在生产环境我会额外写一个监控任务专门扫描ACT_RU_JOB里RETRIES_ 0的记录并按照EXCEPTION_MSG_分组统计。这样至少能第一时间发现“哪类作业持续失败”而不是等业务方投诉。5. 实战排查ACT_RU_JOB常见故障处理实录5.1 定时器长时间不触发曾经有个项目流程配置了定时启动事件部署上去之后怎么都不触发。代码检查了很久最后发现就是配置文件里没有开启异步执行器。这种问题在开发环境不明显因为很多开发同学会用单元测试把流程跑完但一到生产环境定时器全靠活着的执行器来推。排查路径可以这样走确认asyncExecutorActivate是否为true确认流程定义是否处于挂起状态查询ACT_RU_JOB看是否存在对应作业SELECT * FROM ACT_RU_JOB WHERE PROCESS_DEF_ID_ xxx AND TYPE_ timer;看DUEDATE_是否已经过期看LOCK_EXP_TIME_是否已经过期看RETRIES_是否大于0。前两步属于省事检查后四步属于表级定位。大多数情况下问题出在第1步和第5步。5.2 作业重试耗尽不再执行碰到作业“假死”如果确认业务逻辑已修复最简单的方法是调用管理APImanagementService.createJobQuery() .jobId(jobId) .singleResult(); managementService.setJobRetries(jobId, 5); managementService.executeJob(jobId);这也是我最推荐的方式用引擎API去操作作业而不是直接UPDATE数据库。因为引擎里还有缓存和状态直接改库容易造成缓存不一致。但如果是线上紧急情况下作业锁没释放我也会偶尔直接清锁字段UPDATE ACT_RU_JOB SET LOCK_EXP_TIME_ NULL, LOCK_OWNER_ NULL WHERE ID_ xxx;注意这种应急操作必须谨慎最好在业务低峰期执行并且事后要观察引擎日志是否有异常。5.3 用管理API代替手改表如果要在代码里写一个“作业管理接口”我通常会暴露以下几个功能查作业详情手动执行单条作业重置重试次数删除死信作业。其中删除作业要格外小心因为DELETE作业可能意味着对应的事件永远不会触发了。如果这个作业来自未完成的流程实例删掉后流程可能卡在那里。所以删除前一定要先确认流程实例的状态以及它是否被其他表依赖。5.4 常见问题速查表现象可能原因建议处理定时器不触发异步执行器未激活开启asyncExecutorActivate作业一直不执行且RETRIES_0重试次数耗尽修复异常后调用setJobRetries作业被其他节点锁住集群节点崩溃等待锁过期或手动清锁作业积压越来越多线程池配置太小或业务执行太慢调整执行器线程数、优化业务作业执行重复锁超时时间过短加长锁超时时间检查时钟一致性作业执行异常但无堆栈EXCEPTION_STACK_ID_指向的字节数组被清理通过EXCEPTION_MSG_粗略定位这里特别提醒一句集群环境下各节点的系统时间一定要同步。如果某个节点时钟比真实时间快了10分钟它锁定的作业会在10分钟后被其他节点认为是“锁过期”从而重复执行。NTP时间同步看起来小事实际出问题时非常难排查。6. Activiti 6/7“升级拆表”后的ACT_RU_JOB家族6.1 为什么要拆表到了Activiti 6之后原来的一张ACT_RU_JOB被拆成了多张表包括ACT_RU_JOB、ACT_RU_TIMER_JOB、ACT_RU_SUSPENDED_JOB、ACT_RU_DEADLETTER_JOB、ACT_RU_HISTORY_JOB。拆表的核心原因是性能和职责分离。原来所有定时器、消息、信号作业都挤在一张表里遇到大量定时器任务时表的扫描压力会很大。拆开后定时器作业单独放消息作业单独放挂起的、失败的、历史的分门别类查询和管理都更清晰。如果你用的Activiti版本已经高于5.x排查作业问题时要注意别再只盯着ACT_RU_JOB了先确认作业属于哪个生命周期。定时器和消息轮询的失败重试逻辑依然类似但具体在哪个表里需要看引擎版本和错误状态。6.2 新旧版本文档/排查的对应关系在旧版里一条定时器作业执行失败RETRIES_减一还在ACT_RU_JOB里躺着。在新版里失败到达一定次数后作业会被移动到ACT_RU_DEADLETTER_JOB也就是死信表。所以如果你用Activi 6之前的SQL查作业在新版本里会查不到“失败作业”因为它们已经在别的表了。做版本升级时我最深的感触是表面上是表名变了实际上是引擎对“作业生命周期”的管理颗粒度变细了。上线前如果没对比新旧作业表结构线上故障往往就是“表查错了”。7. 我的一些实操心得ACT_RU_JOB这张表说复杂不复杂但真正用好的前提是对引擎的异步机制有敬畏心。我踩过几次坑之后养成了几个习惯第一凡是涉及定时器或异步任务的流程定义都必须确认异步执行器已激活并且把激活配置纳入上线检查清单而不是靠默认值。第二生产环境不允许直接修改作业表的REV_、LOCK_OWNER_这类字段宁可调用管理API也不要在库里乱改。第三监控上不只是看“有没有作业失败”而是看“失败作业是否停留在合理的时间窗口内”。定时任务偶尔失败可以重试但如果某类作业的死亡记录持续增长那一定是业务处理逻辑存在隐患。如果你现在正准备基于Activiti做流程平台建议先把ACT_RU_JOB的字段逐个过一遍再动手设计定时任务。等哪天线上作业卡住你会发现一个清晰的知识框架比满屏的日志排查命令更管用。后面我会继续把剩余的表按同样的方式拆开下一次先聊ACT_RU_EXECUTION。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑