从事流程引擎开发的这些年,我最大的感触是:懂得看 Activiti 那25张表的人,线上通常不会慌。大多数人学 Activiti,是从写 BPMN 文件开始,部署、跑通、审批,一切正常,然后就把框架当成黑盒来用。直到某一天线上流程突然启动不了,或者旧版本流程还在“悄悄运行”,才不得不去翻数据库。可25张表摆在那里,每张表该看什么、怎么关联、哪些字段是坑,官方文档写得太分散,网上资料又各说各话。这篇是“Activiti 数据库25张表”系列的第一讲,先聊整个体系里定位最关键的一张表——ACT_RE_PROCDEF,也就是流程定义表。
如果你时间只够认真搞懂其中一张表,我会建议你优先选它。原因很简单:它是整条流程部署链路的终点,也是运行期、历史期几乎所有表的源头。ACT_RU_EXECUTION、ACT_RU_TASK、ACT_HI_PROCINST,这些表里的PROC_DEF_ID_外键,最终都指向这张表。把它弄明白了,后面再理解其他24张表,就顺了。
1. 一张定义表为什么值得先学:Activiti 25张表的全貌与图鉴
1.1 25张表按前缀分成五个仓库
Activiti 的表命名很有规律,表名前缀本身就是分类。ACT代表 Activiti,中间一段代表表的职责域,整个体系分成五个仓库:
| 前缀 | 仓库名称 | 核心职责 | 代表表 |
|---|---|---|---|
ACT_RE_ | Repository(仓库表) | 存部署记录、流程定义、模型 | ACT_RE_DEPLOYMENT、ACT_RE_PROCDEF、ACT_RE_MODEL |
ACT_RU_ | Runtime(运行时表) | 存正在执行的任务、变量、执行实例 | ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE |
ACT_ID_ | Identity(身份表) | 存用户、组、成员关系 | ACT_ID_USER、ACT_ID_GROUP、ACT_ID_MEMBERSHIP |
ACT_HI_ | History(历史表) | 存已结束流程的各类历史 | ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST |
ACT_GE_ | General(通用表) | 存字节数组、全局属性 | ACT_GE_BYTEARRAY、ACT_GE_PROPERTY |
这套命名的好处是,你一看到表的完整名称,就能判断它属于哪个生命周期阶段。RE是静态资源层面的,RU是动态执行层面的,HI是历史沉淀层面的。ACT_RE_PROCDEF就属于静态资源层,但它和运行期、历史期都有直接关联,所以它是唯一一张“定义表里的运行时核心”。
1.2 部署链路的终端节点
要理解ACT_RE_PROCDEF的分量,先看一条完整的部署链路。当你执行:
repositoryService.createDeployment() .addClasspathResource("processes/leave.bpmn20.xml") .name("请假流程") .deploy();这一步内部实际产生了三种数据:
- 在
ACT_RE_DEPLOYMENT插入一条部署记录,记录“什么时候、以什么名字,整体部署了一批资源”。 - 在
ACT_GE_BYTEARRAY插入 BPMN XML 的二进制内容(如果存在流程图,还会有一条白屏 PNG 或 SVG 的字节记录)。 - 引擎解析 XML,把
<process id="leave" name="请假流程">抽出来,在ACT_RE_PROCDEF插入一条流程定义。
可以这么理解:ACT_RE_DEPLOYMENT是一个“包裹”,ACT_GE_BYTEARRAY是包裹里的“文件”,ACT_RE_PROCDEF是文件里的“关键商品信息”。后面启动流程、查询任务、生成历史,全部围绕“商品信息”这个大目录展开。而PROC_DEF_ID_就是这个大目录的唯一编号。
正因为它是链路终端,所以它天然承载了大量外键引用。运行实例表ACT_RU_EXECUTION.PROC_DEF_ID_指向它;历史流程实例表ACT_HI_PROCINST.PROC_DEF_ID_也指向它。这张表一旦被误删,运行中的流程会立刻失去定义归属,轻则查不到流程版本,重则相关 API 直接报异常。
2. 15个字段逐列拆解:从 ID_ 到 ENGINE_VERSION_ 的设计意图
我实际接触过的 Activiti 5、Activiti 6、以及部分分支版本,ACT_RE_PROCDEF表结构整体稳定。下表是常见版本中会出现的列,我逐个说每个字段“为什么存在”。
| 字段 | 类型 | 含义 |
|---|---|---|
ID_ | varchar(64) | 主键 |
REV_ | int | 乐观锁版本号 |
CATEGORY_ | varchar(255) | 流程分类 |
NAME_ | varchar(255) | 流程名称,来自<process name> |
KEY_ | varchar(255) | 流程业务键,来自<process id> |
VERSION_ | int | 版本号 |
DEPLOYMENT_ID_ | varchar(64) | 所属部署记录 ID,关联ACT_RE_DEPLOYMENT |
RESOURCE_NAME_ | varchar(4000) | BPMN 文件路径 |
DGRM_RESOURCE_NAME_ | varchar(4000) | 流程图片路径 |
DESCRIPTION_ | varchar(4000) | 描述,来自<documentation> |
HAS_START_FORM_KEY_ | tinyint | 是否配置了启动表单 key |
HAS_GRAPHICAL_NOTATION_ | tinyint | 是否包含可视化图形信息 |
SUSPENSION_STATE_ | int | 挂起状态(1 激活,2 挂起) |
TENANT_ID_ | varchar(255) | 租户标识 |
ENGINE_VERSION_ | varchar(255) | 引擎版本标识 |
2.1 业务定位字段:KEY_、NAME_、CATEGORY_、DESCRIPTION_
先说最容易混淆的两对。KEY_来自 BPMN 文件里<process id="leave">的id属性,它才是业务上真正用来定位流程的核心键。比如说:
<process id="leave" name="请假审批流程" isExecutable="true">那么KEY_就是leave,NAME_就是请假审批流程。启动流程时,最常用的runtimeService.startProcessInstanceByKey("leave"),用的就是KEY_。
为什么不用NAME_定位?因为NAME_只是人类可读的展示名称,可以在不同环境里随意改;而KEY_是流程文件的稳定标识,同一个流程在不同环境部署时保持相同。举个实际场景:开发环境的 BPMN 里 name 叫“请假流程”,生产环境的 name 改成“请假审批流程”,如果你用 name 启动流程,环境间行为就会不一致,测试环境验证过的东西上不了生产。按KEY_启动,两边完全一致。
CATEGORY_一般来自 BPMN 文件的targetNamespace,例如http://activiti.org/process。这个字段很少承担业务过滤,但做流程分类统计时偶尔会用到。DESCRIPTION_对应<documentation>标签,通常用来放流程的补充说明。它的信息量比较小,很多团队根本不填。
2.2 生命周期与控制字段:VERSION_、SUSPENSION_STATE_、HAS_START_FORM_KEY_
VERSION_是整张表里第二个核心字段。它的规则是:同一个KEY_每部署一次新版本,VERSION_就加 1,从 1 开始。但只要换了KEY_,新流程的版本号又从 1 开始。所以你在表里看到VERSION_ = 3,不代表这是历史上第三次部署,只代表它是这个KEY_下第三个版本。
SUSPENSION_STATE_控制流程定义能否创建新实例。值为 1 是激活,值为 2 是挂起。挂起后,startProcessInstanceByKey会被拒绝,但已生成的运行中实例不受影响。这个字段是整个流程“启停控制”的第一道闸门,后面我会专门讲它和实例级挂起的区别。
HAS_START_FORM_KEY_决定了启动环节是否存在可配置的表单 key。凡是配置了启动事件上的activiti:formKey属性,这个字段就是 1。从流程引擎角度,它避免了每次启动都要解析 XML 才知道“有没有表单”,直接查这个字段即可。这个细节在后端做“可配置启动表单”功能时非常有用——你可以把这个字段当索引,快速筛出哪些流程支持动态表单。
2.3 关联字段与资源定位:DEPLOYMENT_ID_、RESOURCE_NAME_、DGRM_RESOURCE_NAME_
DEPLOYMENT_ID_是一对多的外键关系:一次部署可能包含多个 BPMN 文件,也就是一条ACT_RE_DEPLOYMENT记录对应多条ACT_RE_PROCDEF记录。反过来说,一条流程定义必然来自某一次部署。
RESOURCE_NAME_记录 BPMN 文件在部署资源里的路径,例如processes/leave.bpmn20.xml。DGRM_RESOURCE_NAME_记录流程图的文件名,例如processes/leave.leave.png。注意,DGRM_RESOURCE_NAME_不一定有值。只有当部署时同时导入了图片文件,或者引擎根据 DI 信息自动生成图形时,这个字段才有内容。它和HAS_GRAPHICAL_NOTATION_配合使用:后者表示这张流程定义是否带有可用图形化展示的信息,前端流程设计器展示流程图时,基本都靠这两个字段定位图片资源。
还有两个字段需要注意。REV_属于乐观锁版本号,每次更新这条记录都会加 1;TENANT_ID_用于多租户场景,单租户部署时通常是空字符串;ENGINE_VERSION_是当系统里激活了 Activiti 5 与 Activiti 6 兼容共存时才会用到的标识,普通部署下多数为 null。
3. 版本递增的完整链路:一次部署在 ACT_RE_PROCDEF 里到底写了什么
3.1 部署三部曲:DEPLOYMENT、BYTEARRAY、PROCDEF
用一次真实部署来追踪。我本地写了一个很简单的请假流程,文件名为leave.bpmn20.xml,里面的<process id="leave">。第一次部署后,三个表的数据大致如下:
ACT_RE_DEPLOYMENT:
| ID_ | NAME_ | DEPLOY_TIME_ |
|---|---|---|
| 1 | 请假流程 | 2025-01-10 10:00:00 |
ACT_GE_BYTEARRAY:
| ID_ | NAME_ | DEPLOYMENT_ID_ | BYTES_ |
|---|---|---|---|
| 2 | processes/leave.bpmn20.xml | 1 | BPMN 二进制内容 |
| 3 | processes/leave.leave.png | 1 | 图片二进制内容 |
ACT_RE_PROCDEF:
| ID_ | KEY_ | NAME_ | VERSION_ | DEPLOYMENT_ID_ |
|---|---|---|---|---|
| 4 | leave | 请假审批流程 | 1 | 1 |
这里的ID_在 Activiti 5/6 里通常长这样:leave:1:4。它把KEY_、VERSION_、自增编号组合成一个可读主键,这也是我在排查线上问题时最常用的一个信息源——看到leave:1:4,我立刻能知道这是哪个流程的第几个版本,以及它在内部表里的自增编号。
3.2 版本号的生成规则与内容去重的边界
第二次部署同一个文件,再查ACT_RE_PROCDEF,注意观察:如果 BPMN 内容完全没变,Activiti 会做“内容去重”——不会新增版本,VERSION_保持在 1,只是ACT_RE_DEPLOYMENT会多一条部署记录。这个行为容易让新手误解:以为每次 deploy 都会产生新版本号。
如果你修改了流程内容,比如在审批节点加了一个条件表达式,那么再次部署后,ACT_RE_PROCDEF会出现:
| ID_ | KEY_ | NAME_ | VERSION_ | DEPLOYMENT_ID_ |
|---|---|---|---|---|
| 4 | leave | 请假审批流程 | 1 | 1 |
| 7 | leave | 请假审批流程 | 2 | 5 |
版本号从 1 跳到 2。这里的关键是,Activiti 通过比对字节内容(ACT_GE_BYTEARRAY.BYTES_)来决定是否生成新版本。只要 XML 字符串有任何差异,哪怕只是加了一行注释,也会生成新版本。
换一个场景:你部署了一个KEY_ = "leave_v2"的流程文件,新定义的VERSION_就从 1 重新开始。这一点在多人协作时很容易踩坑——某位同事把 BPMN 文件的id改了,看起来“部署成功”,实际上它变成了一个新流程,而不是旧流程的新版本,原有的任务变量、业务关联、监听配置全都不在了。
3.3 用实例追一遍实际的 SQL 痕迹
如果你在调试时想亲眼确认这套逻辑,可以打开 Activiti 的 SQL 日志,或者直接在数据库里执行查询。部署后,用这条 SQL 看定义数据:
SELECT ID_, KEY_, NAME_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_ FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave' ORDER BY VERSION_ DESC;第二次部署不同内容后,再执行,你会看到两行记录。整个过程对应的 Java API 也很直观:
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery() .processDefinitionKey("leave") .latestVersion() .singleResult(); System.out.println(processDefinition.getId()); // leave:2:7 System.out.println(processDefinition.getVersion()); // 2latestVersion()这个 API 的背后,就是引擎自动取MAX(VERSION_)。理解了这一点,你以后排查“为什么启动的流程不是我刚部署的那一版”时,第一反应就应该是:先查查是否真的生成了新版本号,再查latestVersion()是否被挂起状态影响。
4. 围绕 ACT_RE_PROCDEF 的日常操作:查询、挂起、删除与清理
4.1 最常用的定义查询(SQL 与 Java API)
日常工作中对这张表最多的操作就是查询。我整理几个高频场景对应的 SQL。
查某个KEY_下的全部历史版本:
SELECT ID_, KEY_, VERSION_, SUSPENSION_STATE_, DEPLOYMENT_ID_ FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave' ORDER BY VERSION_ DESC;查最新且处于激活状态的版本:
SELECT p.* FROM ACT_RE_PROCDEF p WHERE p.KEY_ = 'leave' AND p.SUSPENSION_STATE_ = 1 AND p.VERSION_ = ( SELECT MAX(VERSION_) FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave' );查最近一周部署过的所有流程定义:
SELECT p.ID_, p.KEY_, p.VERSION_, d.DEPLOY_TIME_ FROM ACT_RE_PROCDEF p LEFT JOIN ACT_RE_DEPLOYMENT d ON p.DEPLOYMENT_ID_ = d.ID_ WHERE d.DEPLOY_TIME_ > DATE_SUB(NOW(), INTERVAL 7 DAY);把这些 SQL 和repositoryService对应起来,你会发现,createProcessDefinitionQuery()的processDefinitionKey、latestVersion、active这三个方法,就是上面 SQL 条件的 Java 封装。所以如果你能读懂这张表的 SQL,看 API 文档时会通透很多。
4.2 挂起/激活:定义级与实例级的两层控制
SUSPENSION_STATE_只有两个值:1 表示激活,2 表示挂起。挂起流程定义,最直接的影响是:新流程实例不能再启动。执行以下代码:
repositoryService.suspendProcessDefinitionByKey("leave");此时ACT_RE_PROCDEF里对应记录的SUSPENSION_STATE_变为 2。之后调用startProcessInstanceByKey("leave")会抛出异常,提示该流程定义被挂起。已运行中的流程实例,以及已生成的任务,都不会因为定义挂起而停下。这是很多初学者的认知盲区:挂起定义不等于暂停所有实例。
如果你希望让线上某个正在运行的流程实例整体暂停,你需要操作的是ACT_RU_EXECUTION层,也就是实例级挂起:
runtimeService.suspendProcessInstanceById(executionId);两者的关系可以类比“法律条文被暂停适用”和“某个案件被暂停审理”。定义级挂起针对的是未来,实例级挂起针对的是当下。排查问题时,先分清你要控制的是哪一层,再看对应的表。
还有个细节值得注意:repositoryService里还有activateProcessDefinitionByKey这类反向操作,激活后SUSPENSION_STATE_恢复为 1。但是,如果只挂起了某条具体的流程定义记录,而不是整个KEY_,那么查询时往往需要用processDefinitionId精确指定。在实际项目中,我见过不少因为参数用错,导致“挂起了但还能启动”或“没挂起却启动不了”的诡异现象,追到最后都是定义级和实例级混用了。
4.3 删除与级联:那些容易留“脏数据”的操作
删除流程定义不是简单地DELETE FROM ACT_RE_PROCDEF WHERE ...。Activiti 提供的是部署级删除入口:
repositoryService.deleteDeployment(deploymentId, cascade);注意,这个方法删的是部署记录,但级联参数cascade为true时,会连带着把该部署下的流程定义、以及它们关联的运行数据、历史数据一起清除。cascade = false时,如果这个部署下的流程已经产生过历史数据,删除会抛异常,保护现场。
我在真实项目里见过一种典型的脏数据场景:测试环境下,有人图省事,直接执行 SQL 把ACT_RE_PROCDEF清空,打算“重新部署一套干净的”,结果部署后运行时报错,因为ACT_RU_EXECUTION里的旧实例外键还指向已被删除的定义 ID。流程引擎在解析旧实例的任务节点时拿不到定义,直接抛异常。所以清理表数据的正确姿势是:
- 先用
deleteDeployment(id, true)按部署链路走。 - 或者先删除运行实例和历史实例删除接口,再清理定义。
如果你真的只能在数据库层操作(比如开发库被搞乱了),至少按引用关系从下游往上游删:
DELETE FROM ACT_RU_EXECUTION WHERE PROC_DEF_ID_ LIKE 'leave%'; DELETE FROM ACT_HI_PROCINST WHERE PROC_DEF_ID_ LIKE 'leave%'; DELETE FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave';但这种方式隐患很大,生产环境绝对不建议直接这么干。我的经验是:能走 API 清理,绝不直接改表。
5. 从这张表反推 Activiti 的设计哲学:以及踩过三次的坑
5.1 设计哲学:为什么版本可以共存
ACT_RE_PROCDEF最核心的设计思想,就是用“部署、字节、定义”三层结构,换取版本共存的能力。一个流程被部署多次,产生了多个版本,这些版本在数据上互不干扰。运行中的旧版本流程,可以按旧流程的节点顺序继续执行;新启动的流程,默认走最新版本。
这套设计很像“软件发布”:线上同时运行着 v10 版本的旧进程,v11 已经部署但只对新请求生效。这种灰度理念,对于一个允许长期运行的审批流程来说极其重要——你不可能因为升级流程,就让所有已经跑到一半的审批单作废重走。
理解这个哲学后,你就能理解为什么VERSION_是自增而不是随机的:因为引擎需要唯一定位“某个 KEY 下的第 N 版”,用业务键加版本号,是最直观、最稳定的复合主键逻辑。同时,latestVersion()的语义也依赖自增版本号,如果版本号是随机的,那找出“最新版本”就变成了一件语义模糊的事。
5.2 踩坑实录:测试环境版本爆炸、误删定义、挂起误伤
第一个坑是版本爆炸。自动化测试每跑一轮就部署一次,断言失败重跑又部署一次,几天不清理,ACT_RE_PROCDEF里同一个KEY_就有几十个版本。我不止一次看到测试库里的版本号从 1 一路涨到 60 多。版本多了有什么危害?一是全表查询变慢,二是搞不清当前到底跑的是哪版,三是部署校验时内容比对越来越慢。后来我的习惯是:测试环境的部署代码加上“同 Key 部署前先清理旧版本”的逻辑,或者在 CI 流程里定期执行deleteDeployment。
第二个坑是误删定义。当时线上有一次灰度发布,新流程定义部署失败,查了很久才发现是前一天同事直接手删了ACT_RE_PROCDEF里的部分记录,导致引擎内部校验流程定义存在性时对不上号。从此我给自己定了一条铁律:任何定义表的变更,优先走 API;迫不得已走 SQL,也必须把关联引用一起查出来看一遍再动手。
第三个坑是挂起误伤。有次我打算临时挂起某个旧版本流程定义,让它不再被新实例选中,结果参数写成了按KEY_挂起,把所有版本全都挂上了,线上新流程全部启动失败。这类问题排查时,第一眼就应该去ACT_RE_PROCDEF里确认SUSPENSION_STATE_的值。如果你看到多个版本都是 2,基本可以断定是按KEY_做了全量挂起,而不是只针对某个版本。
5.3 一个后期能救命的小技巧:快照式版本巡检
最后分享一个我在实战里养成的习惯——定期做流程定义“快照巡检”。巡检目标就一个:确认识别线上有几个KEY_、每个KEY_最新版本是什么、资源文件是否完整。SQL 如下:
SELECT p.KEY_, p.VERSION_, p.NAME_, p.RESOURCE_NAME_, p.DGRM_RESOURCE_NAME_, p.SUSPENSION_STATE_, d.DEPLOY_TIME_ FROM ACT_RE_PROCDEF p LEFT JOIN ACT_RE_DEPLOYMENT d ON p.DEPLOYMENT_ID_ = d.ID_ ORDER BY p.KEY_, p.VERSION_;这条 SQL 看起来简单,但它在故障排查时非常管用。流程启动失败,先跑一遍它,基本能快速回答三个问题:这个KEY_是否存在?最新版本是多少?有没有被挂起?如果RESOURCE_NAME_或DGRM_RESOURCE_NAME_为空,说明部署包不完整,流程图无法展示,前端可能异常。
已经不止一次,线上反馈“流程审批页面显示不了流程图”,我第一条 SQL 就是这条,立刻定位到DGRM_RESOURCE_NAME_为空,再顺藤摸瓜查到是部署时漏传了图片资源。这张表的字段不多,但每一条都能在关键时候派上用场。
回到最开始那句话:懂得看这 25 张表的人,线上通常不会慌。而真正吃透ACT_RE_PROCDEF之后,你再看其他表时,会发现自己已经知道该往哪儿查、查什么字段、怎么和外键关联。这张定义表是整个 Activiti 数据世界的入口,把入口打通,后面的路走起来就顺了。