Activiti部署、版本、缓存与生产发布全过程
流程部署不是“把一张图上传到服务器”。它会把 BPMN XML 和其他资源持久化,解析可执行模型,生成流程定义元数据,并让后续流程实例能够绑定到一个不可混淆的定义版本。
商业系统最危险的误区,是认为“部署新版本以后,所有流程都会自动走新图”。实际上,已经运行的实例通常继续绑定旧 processDefinitionId;只有按 key 新启动的实例才通常选择最新激活版本。错误处理版本关系,会造成新旧实例行为不同、变量条件不兼容、任务无人办理,甚至因为级联删除丢失审计数据。
学习目标
完成本页后,你应该能够:
- 区分 Deployment、流程定义、流程实例和业务单据。
- 解释一个 BPMN 文件从部署请求到数据库落表、解析和缓存的完整过程。
- 看懂
deploymentId、processDefinitionId、processDefinitionKey、version和tenantId。 - 解释同 key 版本号怎样产生,以及并发部署为什么需要数据库事务保护。
- 说清按 key、按 definitionId 启动流程的差别。
- 理解旧实例为什么不会因新版本部署而自动迁移。
- 正确使用重复过滤、挂起、激活和删除 API,并说明风险边界。
- 设计代码随包发布、后台上传和受控发布平台三种方案。
- 设计流程发布审查、灰度、回滚、迁移与审计方案。
- 根据引擎表、API 查询、日志和业务表定位部署与版本故障。
一、先区分四个完全不同的对象
flowchart TD
A["BPMN资源:可执行流程模型文件"] --> B["Deployment:一次资源发布批次"]
B --> C["ProcessDefinition:解析后的某个流程定义版本"]
C --> D1["ProcessInstance A:绑定该定义的一次运行"]
C --> D2["ProcessInstance B:绑定该定义的另一次运行"]
E["业务单据:报销单、合同、资产发布单"] --> D1
E --> D2| 对象 | 是什么 | 生命周期 | 常见标识 |
|---|---|---|---|
| BPMN资源 | XML格式的流程模型,可以附带图片、表单等资源 | 随Deployment保存 | 资源名,例如 asset_publish.bpmn20.xml |
| Deployment | 一次部署动作及其资源集合 | 每部署一次通常产生一条记录 | deploymentId |
| ProcessDefinition | 某个BPMN <process> 解析后的可执行定义版本 | 同key可存在多个版本 | processDefinitionId、key、version |
| ProcessInstance | 某个业务单据发起的一次运行 | 从开始事件运行到结束/终止 | processInstanceId |
| 业务单据 | 业务系统的事实和状态 | 由业务系统定义 | 业务主键、businessKey |
一次 Deployment 可以包含多个 BPMN 文件,一个 BPMN 文件也可以包含多个 <process>,所以 Deployment 和 ProcessDefinition 不是一对一关系。
二、五个版本字段必须说准确
假设流程 XML 中定义:
<process id="asset_publish" name="数据资产发布审批" isExecutable="true">
<!-- 节点省略 -->
</process>连续部署三次后,常见元数据可能是:
deploymentId = 2501
processDefinitionKey = asset_publish
version = 3
processDefinitionId = asset_publish:3:2504
resourceName = processes/asset_publish.bpmn20.xml不同版本和数据库 ID 生成器会让具体 ID 形式不同,不应通过字符串拆分 ID 获取业务信息。应使用 ProcessDefinition API 字段。
2.1 deploymentId
deploymentId 标识一次部署批次。它回答的是“这批资源什么时候、以什么名称、由谁发布”,不是“业务流程 key 是什么”。同一次部署中的多个定义会共享 deploymentId。
2.2 processDefinitionKey
流程定义 key 通常来自 BPMN <process id="...">,例如 asset_publish。它是业务层长期识别“同一类流程”的稳定逻辑键。
修改流程名称不一定改变 key;如果把 key 从 asset_publish 改成 asset_release,引擎会把它视为另一条流程线,版本一般从 1 重新开始。
2.3 version
版本号是在相同 key 范围内递增的整数。多租户模式下,版本计算通常还与 tenant 维度相关,具体以使用的 Activiti 版本为准。
版本号不是 Git tag,也不是应用版本。它只能表达引擎数据库中该 key 的定义序号。生产仍应额外记录 Git commit、制品 Digest、发布人、变更单和模型校验结果。
2.4 processDefinitionId
processDefinitionId 唯一指向某一个具体定义版本。已经启动的流程实例通过它绑定定义,因此旧实例不会因为同 key 新版本出现就自动换图。
2.5 businessKey
businessKey 属于流程实例,不属于流程定义。它通常保存业务单据 ID 或稳定业务编号,让流程实例能够反查业务对象:
definition key = 这是什么类型的流程
definition id = 这次运行使用哪个流程版本
business key = 这次运行对应哪张业务单详细业务关联见:业务表与流程一致性。
三、最小可执行BPMN Demo
下面是一个简化的数据资产发布审批流程。实际项目需要补命名空间、候选组、表达式安全和业务监听器治理,本例用于观察部署与版本。
保存为 src/main/resources/processes/asset_publish.bpmn20.xml:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
targetNamespace="https://example.com/processes">
<process id="asset_publish"
name="数据资产发布审批"
isExecutable="true">
<startEvent id="start" name="提交申请"/>
<userTask id="securityReview"
name="安全审核"
activiti:candidateGroups="SECURITY_REVIEWER"/>
<exclusiveGateway id="reviewResult" name="审核结果"
default="toRejected"/>
<endEvent id="approvedEnd" name="审批通过"/>
<endEvent id="rejectedEnd" name="审批拒绝"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="securityReview"/>
<sequenceFlow id="flow2" sourceRef="securityReview" targetRef="reviewResult"/>
<sequenceFlow id="toApproved"
sourceRef="reviewResult"
targetRef="approvedEnd">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[
${approved == true}
]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="toRejected"
sourceRef="reviewResult"
targetRef="rejectedEnd"/>
</process>
</definitions>这里有两个版本契约:
process id="asset_publish"是流程定义 key,发布后不应随意改。- 网关依赖 Boolean 变量
approved。如果新旧代码有时写字符串"true"、有时写 Booleantrue,表达式结果可能不同。
四、部署全过程
flowchart TD
A["应用或发布平台读取BPMN资源"] --> B["RepositoryService创建DeploymentBuilder"]
B --> C["添加BPMN、图片或表单资源"]
C --> D["调用deploy进入引擎Command"]
D --> E["开启或加入数据库事务"]
E --> F["保存Deployment和资源字节"]
F --> G["解析BPMN XML并校验可执行模型"]
G --> H["提取每个process的key和元数据"]
H --> I["查询同key当前最大版本"]
I --> J["生成新ProcessDefinition记录"]
J --> K["提交事务"]
K --> L["定义进入Deployment Cache或按需加载"]4.1 RepositoryService只是门面
业务代码调用 RepositoryService,引擎内部通常把部署封装成 Command,在统一 CommandContext 中使用 Session、EntityManager 和数据库事务完成持久化。不同 Activiti 大版本内部类名可能不同,但“命令上下文统一管理事务和持久化”这一思路稳定存在。
4.2 为什么必须解析XML
BPMN 文件不是图片。引擎要解析:
<process>的 id、name 和 executable。- Start Event、User Task、Service Task、Gateway 和 End Event。
- Sequence Flow 的连接关系和条件表达式。
- 监听器、候选人、异步作业等 Activiti 扩展属性。
XML 格式正确不等于模型业务正确。比如排他网关没有默认线可能仍能部署,但运行时所有条件都不满足就会失败。
4.3 为什么版本号要在数据库事务中产生
两个实例同时部署相同 key 时,如果都先读到当前版本 3,然后都准备写版本 4,会发生冲突。引擎依赖事务、唯一约束或内部部署逻辑保证最终定义记录一致。
生产不能绕过 RepositoryService 直接向引擎表插数据,因为这样会跳过解析、缓存、版本和完整性逻辑。
4.4 deploy返回成功能证明什么
它通常能证明本次部署事务成功提交;不能证明:
- 候选组在企业组织系统中真实存在。
- 所有运行时表达式都能对实际变量正确求值。
- JavaDelegate、监听器和脚本在未来节点执行时一定成功。
- 新版本与旧业务代码、表单字段和消息消费者兼容。
- 新流程已经被业务入口使用。
部署后必须做定义查询、受控实例冒烟和任务流转验证。
五、部署数据写到哪些表
表结构会随 Activiti 版本、历史级别和扩展不同,下面是常见核心表:
| 表 | 典型内容 | 部署阶段作用 |
|---|---|---|
ACT_RE_DEPLOYMENT | 部署ID、名称、时间、分类、租户 | 记录一次部署批次 |
ACT_GE_BYTEARRAY | BPMN XML、流程图等资源字节 | 保存Deployment中的资源 |
ACT_RE_PROCDEF | definitionId、key、version、deploymentId、资源名 | 保存解析后的流程定义元数据 |
ACT_RU_EXECUTION | 运行时Execution/流程实例 | 部署时通常不生成,启动实例后生成 |
ACT_RU_TASK | 当前运行中的用户任务 | 运行到User Task后生成 |
ACT_HI_PROCINST | 历史流程实例 | 启动实例后按历史配置记录 |
对象关系:
flowchart TD
A["ACT_RE_DEPLOYMENT:一次部署"] --> B["ACT_GE_BYTEARRAY:部署资源"]
A --> C["ACT_RE_PROCDEF:一个或多个定义版本"]
C --> D["ACT_RU_EXECUTION:绑定definitionId的运行实例"]
D --> E["ACT_RU_TASK:当前用户任务"]
D --> F["ACT_HI_PROCINST:历史实例轨迹"]
G["业务表"] -->|"businessKey和processInstanceId"| D不要把引擎表当业务表直接修改。查询 SQL 可以作为诊断证据,但状态变更应优先通过公开 API,让引擎同时维护 Execution、Task、Variable、Job、History 和缓存一致性。
六、使用API部署并记录审计信息
下面示例假设 Spring 已注入 RepositoryService。发布接口必须受权限和审计保护,不应直接向普通用户开放。
@Service
public class ProcessDefinitionReleaseService {
private final RepositoryService repositoryService;
private final ProcessReleaseRecordRepository releaseRecordRepository;
public ProcessDefinitionReleaseService(
RepositoryService repositoryService,
ProcessReleaseRecordRepository releaseRecordRepository) {
this.repositoryService = repositoryService;
this.releaseRecordRepository = releaseRecordRepository;
}
@Transactional
public ReleaseResult deployAssetPublish(
String operatorId,
String changeTicketNo,
String gitCommit) {
Deployment deployment = repositoryService.createDeployment()
.name("数据资产发布审批-" + changeTicketNo)
.category("DATA_ASSET")
.addClasspathResource(
"processes/asset_publish.bpmn20.xml")
.deploy();
ProcessDefinition definition = repositoryService
.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.processDefinitionKey("asset_publish")
.singleResult();
if (definition == null) {
throw new IllegalStateException(
"部署已创建,但未找到asset_publish流程定义");
}
ProcessReleaseRecord record = new ProcessReleaseRecord();
record.setDeploymentId(deployment.getId());
record.setProcessDefinitionId(definition.getId());
record.setProcessDefinitionKey(definition.getKey());
record.setProcessDefinitionVersion(definition.getVersion());
record.setOperatorId(operatorId);
record.setChangeTicketNo(changeTicketNo);
record.setGitCommit(gitCommit);
record.setReleasedAt(OffsetDateTime.now());
releaseRecordRepository.save(record);
return new ReleaseResult(
deployment.getId(),
definition.getId(),
definition.getKey(),
definition.getVersion());
}
}这个 Demo 刻意记录了引擎之外的发布审计,因为 Activiti 的 Deployment 元数据通常不足以回答“对应哪个 Git commit、哪个变更单、谁审批发布”。
为什么查询要同时限制deploymentId和key
一次 Deployment 可能包含多个 ProcessDefinition,只按 deploymentId 调 singleResult() 可能因多条结果而失败。增加 key 可以准确定位目标定义。
七、版本号是怎样递增的
假设数据库已有:
| key | version | processDefinitionId | deploymentId |
|---|---|---|---|
asset_publish | 1 | asset_publish:1:1004 | 1001 |
asset_publish | 2 | asset_publish:2:1504 | 1501 |
再次部署相同 key 时,引擎通常生成 version 3。即使 XML 内容完全没改,只要没有启用并命中重复过滤,重复部署也可能产生新版本。
flowchart TD
A["解析出process key等于asset_publish"] --> B["查询该key已有定义"]
B --> C["获得当前最大version等于2"]
C --> D["新定义version设为3"]
D --> E["生成唯一processDefinitionId"]
E --> F["写入ACT_RE_PROCDEF"]流程版本不是手写在 BPMN 文件里的普通字段。不要通过复制 XML 并修改名称来猜版本;部署完成后查询 RepositoryService 才是权威结果。
八、重复部署过滤解决什么
部分 Activiti 版本的 DeploymentBuilder 提供:
Deployment deployment = repositoryService.createDeployment()
.name("数据资产发布审批")
.addClasspathResource("processes/asset_publish.bpmn20.xml")
.enableDuplicateFiltering()
.deploy();它用于避免应用每次启动时因资源未变化仍产生新 Deployment/Definition。其判断通常围绕资源名称和内容与已有部署比较,但精确范围、是否返回已有部署、与 deployment name/tenant 的组合语义应以项目使用的 Activiti 版本源码和测试为准。
它不能替代发布治理:
- 资源只变化一个空格,也可能被视为新内容。
- 外部候选组、表单、JavaDelegate 代码变化不一定体现在 BPMN 字节中。
- 即使 BPMN 字节相同,应用代码和配置也可能不兼容。
- 多实例同时启动仍要考虑自动部署竞争和数据库锁。
商业系统更推荐让发布流水线或单一管理服务拥有部署权,而不是让每个业务实例启动时都竞争部署。
九、按key启动和按definitionId启动的区别
9.1 按key启动
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"asset_publish",
assetId.toString(),
variables);引擎通常选择该 key 下最新的已激活定义。优点是业务代码不必保存最新 definitionId;风险是新定义一发布,新请求可能立即使用新版本。
9.2 按definitionId启动
ProcessInstance instance = runtimeService.startProcessInstanceById(
approvedProcessDefinitionId,
assetId.toString(),
variables);这能精确控制版本,适合灰度发布、租户分批切换或需要审批后才启用新定义的场景,但业务系统必须安全维护“当前允许启动的 definitionId”。
9.3 生产建议:业务发布指针
可以设计自己的流程发布配置表:
CREATE TABLE process_release_binding (
process_key VARCHAR(100) NOT NULL,
scene_code VARCHAR(100) NOT NULL,
tenant_id VARCHAR(100) NOT NULL DEFAULT '',
active_process_definition_id VARCHAR(128) NOT NULL,
active_version INT NOT NULL,
release_status VARCHAR(32) NOT NULL,
change_ticket_no VARCHAR(64) NOT NULL,
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (process_key, scene_code, tenant_id)
);发起流程前读取已发布指针,再按 definitionId 启动。这样“部署资源”和“切换业务流量”成为两个动作,可以先部署、验证,再切换。
代价是发布平台和发起服务需要处理缓存、并发更新和故障恢复,不能过度设计在简单系统中。流程变化少、风险低时,按 key 启动更简单。
十、旧实例为什么继续使用旧版本
启动流程时,实例会保存具体 processDefinitionId。执行后续任务时,引擎根据这个 ID 获取对应模型,不会每走一步重新按 key 查询最新版本。
flowchart TD
A["V1定义已部署"] --> B["实例A启动并绑定V1 definitionId"]
B --> C["部署同key的V2定义"]
C --> D["实例B按key启动并绑定V2"]
C --> E["实例A继续读取V1模型"]
D --> F["实例B执行V2节点和条件"]
E --> G["实例A执行V1节点和条件"]如果实例自动切换到最新图,会出现无法确定的问题:
- V1 当前活动节点在 V2 中已经被删除或改名。
- V2 新增网关依赖变量,但 V1 实例从未保存该变量。
- 并行分支数量变化,Execution Token 无法安全映射。
- 用户正在办理的 Task Definition Key 已改变。
- 历史轨迹无法解释当时实际执行的模型。
因此“旧实例留在旧定义”是保证执行可解释性的必要设计,不是版本功能缺陷。
十一、新版本部署后怎样处理旧实例
通常有三种策略:
| 策略 | 适用场景 | 优点 | 风险/代价 |
|---|---|---|---|
| 旧实例自然结束 | 大多数审批流程 | 最稳,历史可解释 | 新规则不能立即覆盖存量单据 |
| 业务补偿后重开新实例 | 少量高风险实例 | 模型清晰,避免复杂迁移 | 要处理旧实例终止、业务状态和审计关联 |
| 原地迁移实例 | 大量长期实例且必须升级 | 保留实例身份 | 节点映射、变量、Execution和历史非常复杂 |
Activiti 不同大版本对动态状态变更、实例迁移的公开 API 能力不同。不要直接照搬 Flowable、Camunda 或其他版本的迁移 API。迁移前必须确认当前引擎版本能力并建立自动化测试。
原地迁移至少需要回答:
- 当前 Activity ID 在新定义中映射到哪个节点?
- 当前用户任务是保留、取消还是重建?
- 并行 Execution Token 怎样映射?
- 新定义必需变量怎样补齐?
- Timer、Job、Subscription 和 Boundary Event 怎样迁移?
- 历史表怎样标记迁移前后定义?
- 迁移中途失败怎样恢复?
回答不了这些问题时,优先让旧实例自然结束。
十二、流程定义缓存原理
引擎每次推进任务都重新解析数据库中的 BPMN XML,会造成大量 XML 解析和模型构建开销。因此流程定义通常有 Deployment Cache:
flowchart TD
A["执行实例需要processDefinitionId"] --> B{"缓存中是否存在解析模型"}
B -->|"存在"| C["直接取得ProcessDefinition模型"]
B -->|"不存在"| D["根据definition查deployment资源"]
D --> E["从ACT_GE_BYTEARRAY读取BPMN字节"]
E --> F["解析为可执行模型并校验"]
F --> G["写入进程内缓存"]
G --> C关键边界:
- 缓存通常位于每个引擎进程内,不是跨节点共享缓存。
- 应用重启后可以从数据库资源重新加载,不要求缓存持久化。
- 通过 RepositoryService 正常部署和删除时,引擎会维护相关缓存。
- 直接改
ACT_GE_BYTEARRAY或ACT_RE_PROCDEF可能造成数据库与各节点缓存不一致。 - 多节点集群中,不应依赖某个节点内存缓存判断“版本已全局生效”,应查询数据库定义并进行实际启动验证。
十三、挂起与激活定义
当发现新版本有问题但不想立刻删除审计资源时,可以考虑挂起定义。API 在不同版本上略有差异,常见形式如下:
repositoryService.suspendProcessDefinitionById(
processDefinitionId,
false,
null);第二个参数常用于决定是否同时挂起该定义下的运行实例,第三个参数用于延迟生效;请以当前版本方法签名为准。
恢复定义:
repositoryService.activateProcessDefinitionById(
processDefinitionId,
false,
null);必须区分:
- 只挂起定义:阻止或影响新实例启动,已有实例是否受影响取决于 API 参数。
- 同时挂起实例:已有任务推进也可能被阻止,业务影响更大。
- 挂起不是回滚:它不会让新版本实例自动转成旧版本。
- 激活旧定义不一定改变按 key 启动的最新版本选择,必须用实际查询/API 测试确认。
生产回退通常要同时处理“阻止错误版本新启动”和“把业务发布指针切回稳定 definitionId”。
十四、删除Deployment为什么危险
常见 API:
repositoryService.deleteDeployment(deploymentId, false);cascade=false 时,如果该部署定义仍有关联运行实例,引擎通常拒绝删除。这种失败是在保护引用完整性。
级联删除:
repositoryService.deleteDeployment(deploymentId, true);cascade=true 可能删除流程定义及其运行时、任务和历史相关数据,具体范围随版本和历史配置不同。它不是“强制下线新版本”的普通按钮,而是潜在的数据销毁操作。
flowchart TD
A["准备删除Deployment"] --> B["查询包含哪些ProcessDefinition"]
B --> C["统计每个定义的运行实例"]
C --> D{"是否存在运行实例或审计保留要求"}
D -->|"是"| E["禁止直接删除,改用挂起或发布指针回退"]
D -->|"否"| F["确认历史、法规和备份要求"]
F --> G["审批后执行非级联删除或受控清理"]
G --> H["验证定义、资源、缓存和审计记录"]医疗、金融、合同和审计场景通常有长期留痕要求。即使流程已经结束,也不应只因为“表太大”就级联删除。历史归档和清理需要单独的数据生命周期设计。
十五、三种部署方式怎么选
15.1 随应用包自动部署
Spring Boot 集成在部分版本中会扫描约定资源目录并自动部署。具体目录和配置项取决于 Activiti Starter 版本,不应把某个版本属性当成通用标准。
优点:
- BPMN 与应用代码处于同一 Git commit。
- 可以走现有 CI/CD 测试、评审和回滚。
- 适合流程变化不频繁、代码监听器强绑定的系统。
风险:
- 多个应用实例同时启动可能竞争部署。
- 每次重启可能产生重复版本,需明确重复过滤。
- 应用回滚不等于流程实例回滚。
- 自动部署失败可能阻塞应用启动。
15.2 后台直接上传
优点是业务流程可以独立于应用发布;风险是上传者可能绕过代码评审和兼容性测试。
后台至少需要:
- 模型文件类型、大小和 XML 安全解析限制。
- BPMN Schema 与业务规则校验。
- 发布人与审批人分离。
- 版本说明、Diff、变更单和审计日志。
- 测试环境验证和生产二次确认。
- 挂起、切换和回退方案。
15.3 独立受控发布平台
适合多流程、多租户和高审计要求系统。平台负责模型仓库、校验、审批、部署、定义指针、灰度、回退和审计。
它治理能力最强,也引入额外系统复杂度。只有当流程数量、团队数量、变更频率或合规要求足够高时才值得建设。
十六、生产发布应该拆成哪些阶段
flowchart TD
A["模型设计和业务评审"] --> B["静态校验BPMN与表达式契约"]
B --> C["测试环境部署"]
C --> D["创建真实测试变量并跑完整分支"]
D --> E["检查任务、历史、监听器和业务状态"]
E --> F["生产环境只部署资源"]
F --> G["查询并记录definitionId和version"]
G --> H["受控冒烟实例按definitionId启动"]
H --> I{"验证是否通过"}
I -->|"否"| J["挂起新定义并保留证据"]
I -->|"是"| K["切换业务发布指针或允许按key启动"]
K --> L["监控新旧版本实例和任务指标"]16.1 静态校验要检查什么
- 只有预期数量的 Start Event,存在可达 End Event。
- Sequence Flow 没有断链和错误引用。
- 排他网关有完整条件或 Default Flow。
- 并行/包容分支能够正确汇聚,不制造永久等待。
- User Task 候选用户/组来自合法表达式或目录。
- Service Task Delegate 类和表达式白名单存在。
- 变量名、类型、是否必填与旧版本兼容。
- Timer 表达式和时区合理。
- Listener 失败策略、事务边界和幂等明确。
16.2 动态验证为什么不可省
静态模型不能证明外部组织、Bean、数据库和变量真实可用。至少需要按每条商业分支启动测试实例,完成任务并验证:
- 待办出现在正确用户或组。
- 无权用户不能越权完成任务。
- 网关走向与实际变量一致。
- 监听器、消息和业务表更新符合事务设计。
- 结束后运行时记录清理,历史轨迹完整。
十七、兼容性矩阵应该怎样做
流程定义和应用代码往往同时演进。可以建立矩阵:
| 应用版本 | 可启动定义 | 可继续处理的旧定义 | 必需变量契约 | Delegate兼容性 |
|---|---|---|---|---|
| App 2.3 | V3 | V2、V3 | riskLevel:String | 同时保留V2/V3调用的Bean |
| App 2.4 | V4 | V2、V3、V4 | 新增可选dataOwnerId | 不删除旧方法签名 |
如果旧实例还在运行,而应用发布时删除了 V2 Service Task 使用的 JavaDelegate Bean,旧实例走到该节点时会失败。旧 BPMN 留在数据库不等于旧执行代码仍然存在。
生产发布前要统计旧版本运行实例:
List<ProcessDefinition> definitions = repositoryService
.createProcessDefinitionQuery()
.processDefinitionKey("asset_publish")
.orderByProcessDefinitionVersion()
.desc()
.list();
for (ProcessDefinition definition : definitions) {
long running = runtimeService.createProcessInstanceQuery()
.processDefinitionId(definition.getId())
.count();
log.info("processKey={}, version={}, definitionId={}, running={}",
definition.getKey(),
definition.getVersion(),
definition.getId(),
running);
}日志不要输出流程变量中的身份证、手机号、病历、密钥等敏感数据。
十八、多租户版本边界
多租户系统可能让相同流程 key 在不同 tenant 下拥有不同定义。部署时需要显式设置 tenant:
Deployment deployment = repositoryService.createDeployment()
.name("tenant-a数据资产审批")
.tenantId("tenant-a")
.addClasspathResource("processes/asset_publish.bpmn20.xml")
.deploy();查询和启动时也必须带 tenant 语义。不能只按 key 查询后取第一条,否则可能把 A 租户流程发到 B 租户定义。
多租户发布至少要保证:
- tenantId 来自可信服务端上下文,不接受前端任意覆盖。
- Definition、Deployment、实例和业务单据租户一致。
- 管理员跨租户查询有单独权限和审计。
- 缓存键包含 tenant 维度。
- 公共模板和租户定制模板的继承/复制规则清晰。
十九、版本回退的准确做法
流程定义通常不是覆盖更新,因此“回退”并不一定要删除新版本。常见策略:
- 停止错误 definitionId 接受新实例,例如挂起或切换业务指针。
- 把新发起请求重新指向上一个稳定 definitionId。
- 统计已经进入错误版本的实例。
- 根据风险选择继续、补偿后重开或受控迁移。
- 保留错误定义和发布记录,便于审计与复盘。
flowchart TD
A["发现新流程版本异常"] --> B["停止继续启动错误definitionId"]
B --> C["保留Deployment、日志和受影响实例清单"]
C --> D["将新请求切回稳定definitionId"]
D --> E["分类已启动错误版本的实例"]
E --> F1["未产生副作用:取消并重开"]
E --> F2["可继续:增加人工处理和监控"]
E --> F3["必须迁移:执行经验证的迁移方案"]
F1 --> G["核对业务表、任务和历史"]
F2 --> G
F3 --> G“重新部署旧 XML”通常会生成一个更高版本号,例如把 V2 内容重新部署后得到 V4,而不是把最新版本号恢复成 2。这可以作为内容回退,但必须记录 V4 与 V2 内容等价的事实。
二十、故障一:部署失败
现象
deploy()抛出 XML 解析、模型校验或数据库异常。- 应用启动时自动部署失败,导致 Spring Context 启动失败。
- Deployment 记录没有产生,或事务整体回滚。
排查顺序
flowchart TD
A["保存完整异常和根因链"] --> B["确认失败资源名和Git版本"]
B --> C["独立解析XML并检查编码与命名空间"]
C --> D["检查节点ID、连线引用和表达式"]
D --> E["检查数据库连接、权限、锁和唯一约束"]
E --> F["检查引擎版本与BPMN扩展兼容"]
F --> G["修复后在测试环境重新部署并跑分支"]不要只截取最外层 ActivitiException。XML 行列号、SQLState、Constraint 名称和最内层 Cause 才可能指向根因。
二十一、故障二:部署成功但查不到定义
先按 deploymentId 查询:
List<ProcessDefinition> definitions = repositoryService
.createProcessDefinitionQuery()
.deploymentId(deploymentId)
.list();检查:
- BPMN 中
<process isExecutable="true">是否正确。 - 查询的 tenantId、category、key 是否与部署一致。
- 一个 Deployment 是否包含多个定义,错误使用
singleResult()。 - 应用是否连接了错误数据库或错误 schema。
- 事务是否尚未提交,另一个事务暂时不可见。
- 自动部署日志里的 deploymentId 是否属于当前环境。
SQL 只读核对示例:
SELECT ID_, NAME_, DEPLOY_TIME_, TENANT_ID_
FROM ACT_RE_DEPLOYMENT
WHERE ID_ = ?;
SELECT ID_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_, TENANT_ID_
FROM ACT_RE_PROCDEF
WHERE DEPLOYMENT_ID_ = ?;字段名会随版本和数据库大小写策略不同,以当前 schema 为准。
二十二、故障三:新实例仍走旧版本
先取得实例绑定的真实 definitionId:
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceBusinessKey(assetId.toString())
.singleResult();
String actualDefinitionId = instance.getProcessDefinitionId();再查定义:
ProcessDefinition actualDefinition = repositoryService
.createProcessDefinitionQuery()
.processDefinitionId(actualDefinitionId)
.singleResult();可能原因:
- 业务代码按旧 definitionId 精确启动。
- 业务发布指针或本地缓存没有切换。
- 新定义处于 Suspended 状态。
- tenantId 不同,查询的是另一个租户最新版本。
- 实际请求进入了连接另一套数据库的旧应用实例。
- 误以为运行中的旧实例应该自动采用新版本。
排查要记录“启动 API 参数 + 返回 processInstanceId + 实例 processDefinitionId + 定义 version”,不要只看流程名称。
二十三、故障四:旧实例发布后突然失败
最常见的不是 BPMN 资源消失,而是应用执行依赖不兼容:
- 删除了旧 Service Task 使用的 Delegate Bean。
- 修改了 Bean 方法签名或表达式名称。
- 删除旧变量或把类型从 Boolean 改成 String。
- 候选组规则变化后旧任务无人可见。
- 数据库字段迁移不兼容旧流程路径。
- Listener 新代码抛异常,导致任务完成事务回滚。
排查顺序:
- 查实例的
processDefinitionId和 version。 - 下载该 Deployment 的原始 BPMN 资源。
- 定位当前 Activity/Task Definition Key。
- 对照当前应用中 Delegate、表达式和变量契约。
- 查看 Job、Incident/Dead Letter 能力对应的表与管理 API,具体因版本而异。
- 核对业务表是否因事务回滚保持原状态。
二十四、故障五:版本号异常增长
现象:应用每重启一次,version 就增加一次。
常见原因:
- 每个实例启动时自动执行部署。
- 没有启用重复过滤。
- 生成 BPMN 时包含时间戳等非确定内容,每次字节都变化。
- 多副本同时启动,各自执行自动部署。
- 多个服务拥有同一 process key 的部署权。
治理:
- 明确唯一发布者。
- BPMN 制品保持确定性。
- 在测试中验证重复部署行为。
- 监控 key 的版本增长速率和部署来源。
- 发布审计表记录应用实例、commit 和操作人。
不要通过直接删除中间版本“修正版本号”,版本号不连续通常不影响执行,破坏引用和历史才是真正风险。
二十五、故障六:删除定义失败
先判断是否仍有运行实例:
List<ProcessDefinition> definitions = repositoryService
.createProcessDefinitionQuery()
.deploymentId(deploymentId)
.list();
for (ProcessDefinition definition : definitions) {
long count = runtimeService.createProcessInstanceQuery()
.processDefinitionId(definition.getId())
.count();
System.out.printf("definition=%s, running=%d%n",
definition.getId(), count);
}如果仍有实例,非级联删除失败是合理保护。不要因接口报错就把参数改成 true。先确定业务处置、历史保留、备份和审批。
二十六、生产监控与审计
建议监控:
| 指标/事件 | 价值 |
|---|---|
| 每个process key最新version | 发现异常重复部署 |
| 每次Deployment发布人、来源和commit | 建立变更追踪 |
| 每个definitionId运行实例数 | 判断旧代码兼容期和迁移规模 |
| 按definitionId启动失败率 | 发现新版本表达式/依赖错误 |
| User Task创建和完成速率 | 发现流程不再前进 |
| Timer/Async Job失败与重试 | 发现自动节点卡住 |
| 最老运行实例年龄 | 发现僵尸或长期流程 |
| 业务状态与流程状态对账差异 | 发现事务和补偿问题 |
发布日志不要记录完整流程变量。医疗数据、合同金额、身份证、Token 等应脱敏或只记录不可逆摘要和业务 ID。
二十七、发布检查清单
部署前
- [ ]
processDefinitionKey稳定且符合命名规范。 - [ ] BPMN XML、节点引用、网关默认线和表达式通过校验。
- [ ] 新旧变量名称、类型和必填规则兼容。
- [ ] 旧实例依赖的 Delegate、Listener 和表字段仍可用。
- [ ] 候选用户/组在目标环境存在。
- [ ] 定时器、异步任务、消息和外部副作用具有幂等设计。
- [ ] 已统计各旧 definitionId 的运行实例。
- [ ] 已确定新实例按 key 还是按 definitionId 启动。
- [ ] 已准备停止放量、指针回退和存量实例处置方案。
部署中
- [ ] 记录 deploymentId、definitionId、key、version 和 tenantId。
- [ ] 记录制品 Digest、Git commit、变更单、发布人与审批人。
- [ ] 验证数据库事务成功,定义能够查询。
- [ ] 不在多个应用副本中无控制地重复部署。
部署后
- [ ] 用受控 businessKey 启动冒烟实例。
- [ ] 验证候选人、任务、每条网关分支和结束历史。
- [ ] 验证业务表、流程表和审批记录一致。
- [ ] 监控新 definitionId 的启动数、失败率和停留时间。
- [ ] 保留旧定义,不盲目级联删除。
二十八、常见误区
| 误区 | 准确结论 | 不这样理解的后果 |
|---|---|---|
| 部署一次只能有一个定义 | 一次Deployment可包含多个BPMN和多个process | singleResult()异常或漏审定义 |
| key就是definitionId | key标识流程类型,definitionId标识具体版本 | 无法解释版本绑定和灰度 |
| 新版本会覆盖旧版本 | 通常新增定义记录,旧定义仍存在 | 误判旧实例执行路径 |
| 旧实例自动升级 | 实例绑定启动时的definitionId | 新节点/变量映射错误 |
| deploy成功就是发布成功 | 只证明资源持久化和解析事务成功 | 运行时分支、权限和Delegate未验证 |
| 按key启动永远可控 | 新版本部署后可能立即被选中 | 未验证版本直接承接生产流量 |
| 挂起等于回滚 | 挂起限制执行,不能迁移已有实例 | 错误版本存量实例无人处理 |
| cascade删除只是清理文件 | 可能删除运行时和历史数据 | 审计与业务证据不可恢复 |
| 可以直接改ACT表修复 | 会绕过Execution、History、Job和缓存规则 | 数据与缓存不一致,流程进一步损坏 |
| BPMN留在库里旧实例就安全 | 旧实例还依赖当前应用代码和外部系统 | Delegate删除后旧流程运行失败 |
二十九、面试标准回答
Activiti部署后写了什么
RepositoryService部署时把一次发布记录写入Deployment,把BPMN等资源保存为字节资源,解析每个可执行process,并按key生成递增版本的ProcessDefinition元数据。流程实例尚未启动时通常不会产生运行时Execution和Task。执行时引擎可从进程内Deployment Cache获取解析模型,缓存没有时再从数据库资源加载;因此不能直接修改ACT表,否则可能破坏版本、关联和缓存一致性。
新流程版本部署后旧实例怎么办
旧实例通常继续绑定启动时的processDefinitionId,不会自动切到同key最新版本。这保证当前节点、变量、并行Execution和历史轨迹可解释。生产通常让旧实例自然结束,同时保持旧Delegate和变量契约兼容;若必须迁移,要明确节点、任务、变量、Timer和并行Token映射,并经过专项测试,不能直接改表。
按key和按definitionId启动有什么区别
按key启动通常选择该key最新激活定义,代码简单,但新版本部署后可能立即承接新实例;按definitionId能精确控制版本,适合先部署后灰度或租户分批切换,但需要业务发布指针和并发治理。无论哪种,都要把实例实际绑定的definitionId和业务businessKey记录清楚。
Activiti流程怎么回滚
流程回退不是覆盖数据库版本。先阻止错误definitionId继续接收新实例,把新请求切回上一稳定definitionId,再分类处理已经启动的错误版本实例:无副作用时取消重开,可继续时人工补偿,确需迁移时执行经验证的迁移方案。旧XML重新部署通常得到更高version,只是内容回退。Deployment级回退不能自动撤销业务表、消息和外部系统副作用。
为什么不能随便cascade删除Deployment
级联删除可能同时移除定义、运行实例、任务和历史数据,范围取决于版本和历史配置。它会破坏业务关联和审计证据,尤其不适合作为新版本下线手段。应先统计运行实例、确认法规和历史保留要求,优先挂起定义或切换业务发布指针,历史清理走独立归档与审批流程。
三十、学习实验与验收
建议在本地测试数据库完成:
- 部署 V1,查询 Deployment、资源和 ProcessDefinition 三类记录。
- 启动实例 A,记录其 definitionId 和 businessKey。
- 修改任务名称但保持 key,部署 V2。
- 启动实例 B,证明 A 仍绑定 V1、B 绑定 V2。
- 查询两个版本运行实例数,验证兼容矩阵代码。
- 尝试非级联删除 V1 Deployment,观察运行实例引用保护。
- 挂起 V2,验证按 key 和按 ID 启动行为,以当前 Activiti 版本实测为准。
- 模拟业务发布指针切回 V1,验证新实例版本。
- 重启引擎进程,证明定义可从数据库重新加载而不是依赖旧内存缓存。
- 删除旧 Delegate Bean 的测试替身,证明 BPMN 资源存在仍不足以保证旧实例可执行。
验收时必须能回答:
- 一次 Deployment 为什么可能生成多个 ProcessDefinition?
- version、key、definitionId 和 deploymentId 各自解决什么身份问题?
- 重复过滤能防什么,不能防什么?
- 为什么旧实例不自动采用新定义?
- 缓存丢失后模型怎样恢复?
- 挂起、内容回退、实例迁移和级联删除有什么区别?
- 新版本部署成功但业务仍走旧流程,证据从哪里取?
- 为什么直接改 Activiti 表是一种高风险修复?
