做流程引擎集成的人,第一关基本都是被Activiti那25张表绕晕。ACT_GE_BYTEARRAY、ACT_HI_ACTINST、ACT_RU_IDENTITYLINK……光看前缀就够喝一壶。我的建议始终是别急着背全表,先抓三张主表:ACT_RE_PROCDEF看流程模板,ACT_RU_EXECUTION看运行现场,ACT_HI_PROCINST看历史轨迹。把这三张串起来,整个工作流引擎的骨架就立住了。今天这一篇,先把第一张ACT_RE_PROCDEF流程定义表彻底拆开。
ACT_RE_PROCDEF属于RE(Repository)仓库模块,记录的是流程定义的元数据。你在设计器里画好的BPMN流程图,部署进引擎后会被解析,然后把“这个流程叫什么、key是什么、是第几个版本、从哪个文件来的、当前能不能启动”这些信息写进这张表。后续每次调用runtimeService.startProcessInstanceByKey启动流程,引擎都要先到这张表查出对应记录,再据此创建流程实例。所以这张表就是流程模板的“登记簿”,没有它,一切流程都跑不起来。
这篇文章适合三类人细读:第一次集成Activiti、部署后对着空表发愁的新手;生产环境流程启动报错、急着定位是版本问题还是挂起问题的排障者;以及对工作流引擎做二次开发、想理清内部数据模型的进阶开发者。我会先交代25张表的整体分类,再把ACT_RE_PROCDEF的字段、生命周期、常用SQL和排障经验逐个讲透,尽量讲清楚每个设计背后的原因,而不是只给你一张字段表。
1. 先摸清Activiti 25张表的家底
1.1 按前缀分组,整套表就没那么吓人
Activiti所有表都遵循“ACT_前缀 + 业务域”的命名规则。前缀拆开就是含义:
| 前缀 | 全称含义 | 代表表 | 核心作用 |
|---|---|---|---|
| ACT_GE_* | General 通用 | ACT_GE_BYTEARRAY、ACT_GE_PROPERTY | 二进制内容、引擎全局属性 |
| ACT_RE_* | Repository 仓库 | ACT_RE_DEPLOYMENT、ACT_RE_MODEL、ACT_RE_PROCDEF | 部署、模型、流程定义等静态资源 |
| ACT_RU_* | Runtime 运行时 | ACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE、ACT_RU_JOB等 | 流程实例运行期的执行流、任务、变量、作业 |
| ACT_HI_* | History 历史 | ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST、ACT_HI_VARINST等 | 已结束或进行中实例的审计数据 |
| ACT_ID_* | Identity 身份 | ACT_ID_USER、ACT_ID_GROUP、ACT_ID_MEMBERSHIP、ACT_ID_INFO | 用户、组、成员关系、账号信息 |
这套命名不是随便起的。Java端源码里每个域都有对应的EntityManager,比如ProcessDefinitionEntityManager对应ACT_RE_PROCDEF,ExecutionEntityManager对应ACT_RU_EXECUTION。看到前缀就能猜出表的大致职责,这是最快的记忆方式。这里多说一句:不同小版本的25张表会有点出入。比如Activiti 5.x里作业表通常就是ACT_RU_JOB一张,到6.x以后才拆出ACT_RU_TIMER_JOB、ACT_RU_DEADLETTER_JOB、ACT_RU_SUSPENDED_JOB、ACT_RU_HISTORY_JOB这些表。你拿着5.x的文档去查6.x的库,少几张表很正常,先认前缀再看版本。
之所以先强调RE组,是因为所有流程实例都是从定义来的,而ACT_RE_PROCDEF就是RE组里最核心的那张表。把这张表研究透,后面看运行表和历史表都会顺很多。
1.2 ACT_RE_PROCDEF 在RE组里的定位
RE组内部一共三张表,职责差别很大。ACT_RE_DEPLOYMENT记录一次部署动作,你上传单个BPMN文件也好、上传zip包也好,都会生成一个全局部署ID挂在它下面。ACT_RE_MODEL存的是设计器里保存的模型数据,偏向“设计态”。ACT_RE_PROCDEF则是在部署动作完成以后,引擎解析BPMN得到的“运行态模板”。
用一个生活化的类比:BPMN文件相当于一份户型图纸,ACT_RE_DEPLOYMENT是“图纸归档记录”,ACT_RE_PROCDEF是根据图纸烧制好的模型。以后每个流程实例,都是按这个模型“打印”出来的成品。一次部署可以包含多张图纸,所以一个DEPLOYMENT_ID下可能解析出多条流程定义记录;同一个流程key反复部署,又会生成多个版本的PROCDEF记录。理解了这层关系,再看字段就不容易糊涂。
2. ACT_RE_PROCDEF 字段逐个拆解
2.1 ID_ 和 REV_:主键和乐观锁,决定并发安全
ID_是表主键,默认形如leave:1:125004,由流程key、版本号和引擎内部生成的ID拼接而成。为什么这么设计?因为这个主键人眼可读性极强——看到leave:3:17502,不用join任何表就知道这是请假流程第3版,排查问题非常省事。而且“key:version”天然唯一,再拼一个内部序号,基本不存在重复的可能。实际SQL里经常能看到直接用ID_字符串做关联条件,这也是Activiti生态里的常见写法。
REV_是乐观锁字段,初始值为1,每次UPDATE都会把旧值带上再自增。引擎执行更新时,实际SQL是UPDATE ACT_RE_PROCDEF SET ... WHERE ID_=? AND REV_=?,如果影响行数为0,就说明这条记录已被其他事务改过,直接抛OptimisticLockingException。这个机制避免了两边同时挂起同一个流程定义导致状态互相覆盖的问题。所以看到这类异常别慌,先排查是不是两个管理端窗口在同时对同一条流程定义做操作,业务代码里加互斥锁或者失败重试,比手工改数据库REV_靠谱得多。
2.2 KEY_、NAME_、VERSION_、CATEGORY_:流程定义的身份证
KEY_是流程定义的核心业务标识,对应BPMN文件里<process id="leave">的id,不是文件名。这一点很多人栽过跟头:文件名可能叫leave.bpmn20.xml,但里面process元素的id是leaveProcess,那KEY_就是leaveProcess。启动流程时调startProcessInstanceByKey("leave"),引擎找不到定义,直接报“流程定义不存在”。所以拿到一个流程文件,先打开看process的id,别凭文件名猜KEY_。
NAME_是流程展示名称,取自process的name属性,比如“请假流程”。这个字段不参与引擎逻辑,主要是给人看的。VERSION_是版本号,同一KEY下第一次部署是1,第二次部署是2,依次递增。引擎不会去比较两次部署的文件内容是否一致,只要你调用了deploy(),就会在ACT_RE_PROCDEF里新插一条更高版本的记录。这是Activiti版本机制的核心,背后是“不可变历史”的设计原则:已经跑起来的流程实例,必须能完整追溯它启动时用的定义,绝不能把旧定义覆盖掉。
CATEGORY_默认取BPMN根节点targetNamespace的值,很多公司拿它做流程分类或应用归属统计。如果你部署时通过DeploymentBuilder设置了category,实际存储以设置值为准。但要注意模板的targetNamespace如果都是同一个地址,那么表里所有流程定义的CATEGORY_都会是同一个值,做统计报表时要留意这个坑,必要时在部署前统一改写。
2.3 DEPLOYMENT_ID_、RESOURCE_NAME_、DGRM_RESOURCE_NAME_:从定义追溯到文件
DEPLOYMENT_ID_指向ACT_RE_DEPLOYMENT.ID_。一次部署包含多个流程文件时,多个流程定义的DEPLOYMENT_ID_相同;反过来,一个流程定义必然只属于一次部署。排查问题的时候,从PROCDEF出发找到DEPLOYMENT_ID_,再顺着部署ID去ACT_GE_BYTEARRAY里捞原始文件,这是一条非常顺的链路。
RESOURCE_NAME_是被解析的BPMN资源名,通常是classpath下的相对路径,比如processes/leave.bpmn20.xml。DGRM_RESOURCE_NAME_是配套的流程图资源名,比如processes/leave.leave.png。如果部署包没带图片,DGRM_RESOURCE_NAME_一般为NULL。注意这两个字段只存名字,不存文件内容。文件内容的二进制字节都存在ACT_GE_BYTEARRAY表里,靠DEPLOYMENT_ID_和NAME_关联去取。实际开发里要下载某条流程定义的原始XML,或者把流程图展示到前端页面上,靠的就是这组字段。
2.4 描述与状态辅助字段:DESCRIPTION_、HAS_START_FORM_KEY_、HAS_GRAPHICAL_NOTATION_、SUSPENSION_STATE_、TENANT_ID_、ENGINE_VERSION_
DESCRIPTION_存流程描述,一般从BPMN文档元素提取。这个字段有长度限制,别往里塞大段JSON,短描述放这里没问题,复杂扩展信息建议放到业务表。HAS_START_FORM_KEY_标记流程是否配置了启动表单,1表示startEvent设置了formKey,引擎在启动流程前可能先拉取表单定义;0表示直接启动即可。判断一个流程是否要“先弹表单再发起”,看这个字段最直接。
HAS_GRAPHICAL_NOTATION_标记定义是否包含图形坐标信息。部署包带PNG,或者BPMN文件里嵌了DI(Diagram Interchange)信息,这个值才是1。想判断能不能给用户展示流程图,先看这个字段,省得每次都去读字节再尝试转图片。
SUSPENSION_STATE_是流程定义状态,1为激活,2为挂起。挂起后的定义不能用来启动新实例,但是已经跑起来的实例不受影响。TENANT_ID_是多租户标识,默认是NULL。如果你做SaaS平台,部署时通过tenantId区分租户,那所有查询都必须在API上传租户ID,否则很可能查不到定义。ENGINE_VERSION_是引擎内部版本标记,一般不用碰。
字段在不同版本里有细微出入。比如早期版本可能没有TENANT_ID_和ENGINE_VERSION_,升级时会由官方脚本自动补列。
3. 流程定义的生命周期:从部署到被引用
3.1 部署一个流程时,引擎在表里到底做了什么
以最常见的单文件部署为例,代码就三行:
repositoryService.createDeployment() .name("请假流程") .addClasspathResource("processes/leave.bpmn20.xml") .deploy();就这么一句deploy(),引擎内部做了几件大事。第一步,读取XML并解析成BpmnModel,校验节点、连线、事件定义是否合法。第二步,把原始XML字节写入ACT_GE_BYTEARRAY,NAME_是processes/leave.bpmn20.xml,DEPLOYMENT_ID_联到新生成的部署ID。第三步,在ACT_RE_DEPLOYMENT插一条部署记录。第四步,遍历BpmnModel里所有process元素,逐个写入ACT_RE_PROCDEF——首次部署VERSION_=1,第二次部署同key就变成2、3、4这么叠上去。第五步,触发流程定义相关的监听器。
部署完成后你可以直接查表验证:
SELECT ID_, KEY_, NAME_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_, SUSPENSION_STATE_ FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave' ORDER BY VERSION_ DESC;我习惯在测试环境每次部署后都跑一遍这条SQL,确认版本号和资源名都对了再继续。这里要特别强调一个理念:流程定义记录是“只读模板”。想改流程逻辑,不能直接UPDATE这条记录,只能重新部署一份新版本BPMN。这个设计深远地影响了后续所有操作——版本会越积越多,旧实例要留定义快照,生产环境不能随随便便物理删除PROCDEF。
3.2 设计态模型与运行态定义的关系
ACT_RE_MODEL放的是设计器里保存的模型数据,模型本身的JSON/XML源文件也一样塞在ACT_GE_BYTEARRAY里。模型通过一次“部署”动作,才会生成对应的ACT_RE_PROCDEF记录。同一个模型可以被反复部署,每部署一次,PROCDEF就多一个高版本。所以模型和定义不是一一对应,而是“一对多”的关系;反过来,直接从XML部署的流程,可能压根没有MODEL记录。
做流程平台时,经常遇到“模型修改后重新部署”的操作。正确的做法是先更新模型元数据,再调一次repositoryService完成部署,让引擎生成新版本定义。旧版本定义不建议立刻物理删除,除非你已经确认所有关联实例都结束且不需要历史追溯。实际项目里我见过有人图省事直接在数据库里删PROCDEF记录,结果历史流程详情页打不开,审批记录里的流程轨迹全部断链。这个代价比留着几行旧数据大得多。
3.3 版本机制:一个KEY、多个VERSION,启动到底用哪个
默认情况下,startProcessInstanceByKey会使用该KEY下版本号最大的那条定义。引擎内部的查询逻辑大致是:按KEY_匹配,按VERSION_降序,取第一条,同时SUSPENSION_STATE_必须是激活。但有个容易被忽视的细节:如果你把最新版本挂起了,启动流程会直接报“流程定义被挂起”,引擎不会自动回退到上一个激活版本。
所以线上升级流程时,想保留旧版本供紧急回退,业务代码里一定要显式指定processDefinitionId,而不是依赖默认最新版。我在实际项目里会在业务单据表增加一个processDefinitionId字段,用户发起流程时用这个ID启动。这样就算新版本发布后出了严重问题,存量单据还能按原版本继续走,不用全员卡死。
3.4 挂起与激活的实战场景
挂起和激活通过repositoryService操作:
// 挂起指定key的所有版本定义,cascade=true会级联挂起运行中的实例 repositoryService.suspendProcessDefinitionByKey("leave", true, null); // 激活 repositoryService.activateProcessDefinitionByKey("leave", true, null);挂起定义本身,已存在的运行实例不受影响,新实例进不来。cascade参数一旦传true,正在跑的实例也会被挂起,用户提交任务直接报错,这个操作相当危险。生产环境常见的做法是:先只挂起定义,把新入口堵住,再人工评估存量实例,能正常走完的让它走完,走不完的单独处理。等确认没问题了,再激活定义恢复入口。SUSPENSION_STATE_就在1和2之间切换,查询可用定义时别忘了过滤这个字段。
4. 典型场景实操:用SQL和API操作ACT_RE_PROCDEF
4.1 查询当前可用的最新版本定义
日常工作里最常用的就是“查某个key的当前版本”。SQL写法:
SELECT p.ID_, p.KEY_, p.NAME_, p.VERSION_, p.DEPLOYMENT_ID_, p.RESOURCE_NAME_, p.DGRM_RESOURCE_NAME_, p.SUSPENSION_STATE_ FROM ACT_RE_PROCDEF p WHERE p.KEY_ = 'leave' AND p.SUSPENSION_STATE_ = 1 ORDER BY p.VERSION_ DESC LIMIT 1;用API更直观:
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery() .processDefinitionKey("leave") .latestVersion() .singleResult();这里有个坑:latestVersion()只按版本号最大取一条,不会自动过滤挂起状态。如果最新版被挂起了,你还能查到它,但启动时会报错。所以业务代码里最好再补一个.suspended()条件,或者干脆自己指定processDefinitionId。
4.2 根据部署ID捞BPMN原始内容
要下载流程定义的原始XML,光查PROCDEF不够,得join部署表和字节数组表:
SELECT p.ID_ AS PROC_DEF_ID, p.KEY_, p.VERSION_, d.ID_ AS DEPLOYMENT_ID, b.NAME_ AS RESOURCE_NAME, b.BYTES_ FROM ACT_RE_PROCDEF p LEFT JOIN ACT_RE_DEPLOYMENT d ON p.DEPLOYMENT_ID_ = d.ID_ LEFT JOIN ACT_GE_BYTEARRAY b ON b.DEPLOYMENT_ID_ = d.ID_ WHERE p.ID_ = 'leave:3:17502';ACT_GE_BYTEARRAY里的BYTES_字段是blob,不同数据库驱动返回的类型不一样。要在Java里读,直接走API更省事:
InputStream is = repositoryService.getResourceAsStream(deploymentId, resourceName);只要拿到DEPLOYMENT_ID_和RESOURCE_NAME_,两行代码就能把原始文件读出来。我通常把这段逻辑封装成“流程定义详情查询”的接口,前端点开某个版本就能看到XML和高亮流程图。
4.3 通过API动态部署并设置分类、租户
实际项目中更多是上传zip包动态部署:
DeploymentBuilder builder = repositoryService.createDeployment() .name("月度发布") .addZipInputStream(new ZipInputStream(file.getInputStream())) .tenantId("租户A"); if (StringUtils.isNotBlank(category)) { builder.category(category); } Deployment deployment = builder.deploy();addZipInputStream会把zip里的多个BPMN和PNG一起解压注册。这个场景下,ACT_RE_PROCDEF会为zip里每个流程文件分别生成一条记录,共用一个DEPLOYMENT_ID。设置tenantId后,PROCDEF的TENANT_ID_字段就有值了。后面所有查询要是忘了带租户条件,大概率查不到任何流程定义。我建议把租户ID设计成必传参数,而不是默认查全部,这样才能避免SAAS环境下串数据。
4.4 读流程图:HAS_GRAPHICAL_NOTATION_与BYTEARRAY的配合
展示流程图是工作流系统的标配功能。很多部署包没带PNG,Activiti引擎会基于BPMN里的DI坐标自动生成一张图。自动生成的图能用,但长得很简陋,审批人节点、网关颜色都不可控。所以我的建议是,凡是需要给用户看流程图的业务,部署包一定要带上设计器导出的PNG,并且保证DGRM_RESOURCE_NAME_字段有值。
读取图片的推荐顺序是:先看HAS_GRAPHICAL_NOTATION_是否为1,再去ACT_GE_BYTEARRAY按资源名取字节,最后转成图片流输出。不要每次请求都现查现读然后解析图片,那样数据库开销和IO开销都大,可以把解析结果缓存到本地或Redis,流程定义不变就复用缓存。
5. 常见问题与排查技巧实录
5.1 部署成功但ACT_RE_PROCDEF查不到数据
这是新手最高频的疑问。明明代码没报错,控制台还打印了部署成功,结果一查PROCDEF是空的。按顺序排查:首先查ACT_RE_DEPLOYMENT有没有记录,没有就说明部署动作压根没落库。其次查ACT_GE_BYTEARRAY有没有字节数据,可能是文件没被扫描到,比如资源路径写错、zip包内嵌目录不对。再检查是不是多租户模式,API查询没传tenantId,默认过滤掉了。还要确认你连的是不是同一个库,我见过本地配置连了测试库、拿生产库去查的哭笑不得案例。
另外一个隐蔽原因:BPMN解析失败。如果流程文件不是标准bpmn20.xml,或者包含了引擎无法识别的扩展元素,部署阶段可能抛异常并整体回滚,表面上看没报错,实际事务什么都没写。这时候要打开引擎日志找“Error while parsing process definition”之类的关键行。
5.2 启动流程报“process definition does not exist”
这个报错信息在英文环境里可能是“no process definition found with key”。按优先级排查以下原因:
第一,KEY_写错了。KEY_取自BPMN里process的id,不是文件名,也不是流程显示名。第二,流程定义被挂起。挂起的定义虽然还能查到,但启动会被拒绝。第三,多租户下没传tenantId,引擎把租户条件加上以后匹配不到。第四,定义被删除了。如果部署记录被级联删除,PROCDEF也会跟着消失。
排查时先跑这条SQL:
SELECT ID_, KEY_, VERSION_, SUSPENSION_STATE_, TENANT_ID_ FROM ACT_RE_PROCDEF WHERE KEY_ = '你填的key';有数据再看SUSPENSION_STATE_和TENANT_ID_,十有八九问题就浮出水面了。
5.3 删除部署的连带影响
repositoryService.deleteDeployment(deploymentId, true)第二个参数传true表示级联删除。级联删除会连带删掉PROCDEF、BYTEARRAY,以及与该部署相关的运行实例数据,操作非常危险。即使不传true,只删部署,PROCDEF记录通常也会被一并清掉,因为流程定义不能脱离部署独立存在。
但在删除之前,必须确认没有正在运行的流程实例:
SELECT COUNT(*) FROM ACT_HI_PROCINST WHERE PROC_DEF_ID_ = 'leave:1:125004' AND END_TIME_ IS NULL;这个统计口径用ACT_HI_PROCINST更稳妥。ACT_RU_EXECUTION里既有主执行流也有子执行流,直接用PROC_DEF_ID_统计很容易把同一个实例重复算好几遍,误导判断。确认结果大于0的情况下,别删。
5.4 版本不断累积,表膨胀怎么办
长时间运行的流程平台,ACT_RE_PROCDEF会随着每次发布不断变多。有些公司一年发几百次版,两年下来这张表几千条是少的,几十万条也不稀奇。我见过一张PROCDEF膨胀到几百万行的库,直接拖垮了流程定义查询接口。
合理的清理策略是保留每个KEY最近的三到五个版本,把更老的版本归档。第一步,查老版本有没有运行中的实例;第二步,把老版本的部署包和XML内容存到对象存储做离线归档;第三步,删除对应的ACT_RE_PROCDEF记录、ACT_RE_DEPLOYMENT记录、ACT_GE_BYTEARRAY字节。这套流程建议做成定时任务,每个月跑一次,而不是等线上卡到不行再手工清。归档时注意保留历史实例的轨迹数据,历史表ACT_HI_PROCINST仍然需要能查到流程轨迹,只是不能再依赖PROCDEF这张表做实时联动。
5.5 版本升级导致表结构兼容问题
Activiti从5.x升到6.x,ACT_RE_PROCDEF会新增字段,比如ENGINE_VERSION_,可能还会调整部分字段长度。如果在生产库上直接拿新版本引擎跑旧表结构,最常见的报错是“Unknown column”或者“Column not found”。反过来,旧版本代码连新库也可能出现查询时找不到列的问题。
处理办法只有一个:按官方升级脚本逐版本执行,不能跳版本,更不能图省事手工ALTER TABLE。升级前先在测试环境完整跑一遍所有版本的脚本,模拟全量数据迁移,确认查询性能和数据兼容性都没问题再上生产。我见过有人跳版本直接改库,结果历史数据里的ID_格式对不上,导致流程详情无法关联,那个排查过程比写业务代码痛苦多了。
最后再分享一点个人体会。这套表我前前后后查了很多次,真正让技术方案变清晰的,不是把字段背下来,而是想通ACT_RE_PROCDEF和业务系统之间的关系。很多项目一开始接入流程引擎,出现启动找不到定义、版本混乱的问题,本质上都是没理解这张表是“只读模板”,更新流程只能走新版本部署。把这条主线想通,后面看ACT_RU_EXECUTION、ACT_HI_PROCINST这些表都会顺很多。如果你还在研究阶段,建议直接在测试库部署一个最简单的Demo,反复对ACT_RE_PROCDEF做部署、挂起、激活、删除操作,配合本文的SQL看数据变化,比自己闷头翻文档快十倍。