Skip to content

Activiti部署、版本、缓存与生产发布全过程

流程部署不是“把一张图上传到服务器”。它会把 BPMN XML 和其他资源持久化,解析可执行模型,生成流程定义元数据,并让后续流程实例能够绑定到一个不可混淆的定义版本。

商业系统最危险的误区,是认为“部署新版本以后,所有流程都会自动走新图”。实际上,已经运行的实例通常继续绑定旧 processDefinitionId;只有按 key 新启动的实例才通常选择最新激活版本。错误处理版本关系,会造成新旧实例行为不同、变量条件不兼容、任务无人办理,甚至因为级联删除丢失审计数据。

学习目标

完成本页后,你应该能够:

  1. 区分 Deployment、流程定义、流程实例和业务单据。
  2. 解释一个 BPMN 文件从部署请求到数据库落表、解析和缓存的完整过程。
  3. 看懂 deploymentIdprocessDefinitionIdprocessDefinitionKeyversiontenantId
  4. 解释同 key 版本号怎样产生,以及并发部署为什么需要数据库事务保护。
  5. 说清按 key、按 definitionId 启动流程的差别。
  6. 理解旧实例为什么不会因新版本部署而自动迁移。
  7. 正确使用重复过滤、挂起、激活和删除 API,并说明风险边界。
  8. 设计代码随包发布、后台上传和受控发布平台三种方案。
  9. 设计流程发布审查、灰度、回滚、迁移与审计方案。
  10. 根据引擎表、API 查询、日志和业务表定位部署与版本故障。

一、先区分四个完全不同的对象

mermaid
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 中定义:

xml
<process id="asset_publish" name="数据资产发布审批" isExecutable="true">
    <!-- 节点省略 -->
</process>

连续部署三次后,常见元数据可能是:

text
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 或稳定业务编号,让流程实例能够反查业务对象:

text
definition key = 这是什么类型的流程
definition id  = 这次运行使用哪个流程版本
business key   = 这次运行对应哪张业务单

详细业务关联见:业务表与流程一致性

三、最小可执行BPMN Demo

下面是一个简化的数据资产发布审批流程。实际项目需要补命名空间、候选组、表达式安全和业务监听器治理,本例用于观察部署与版本。

保存为 src/main/resources/processes/asset_publish.bpmn20.xml

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>

这里有两个版本契约:

  1. process id="asset_publish" 是流程定义 key,发布后不应随意改。
  2. 网关依赖 Boolean 变量 approved。如果新旧代码有时写字符串 "true"、有时写 Boolean true,表达式结果可能不同。

四、部署全过程

mermaid
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_BYTEARRAYBPMN XML、流程图等资源字节保存Deployment中的资源
ACT_RE_PROCDEFdefinitionId、key、version、deploymentId、资源名保存解析后的流程定义元数据
ACT_RU_EXECUTION运行时Execution/流程实例部署时通常不生成,启动实例后生成
ACT_RU_TASK当前运行中的用户任务运行到User Task后生成
ACT_HI_PROCINST历史流程实例启动实例后按历史配置记录

对象关系:

mermaid
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。发布接口必须受权限和审计保护,不应直接向普通用户开放。

java
@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 可以准确定位目标定义。

七、版本号是怎样递增的

假设数据库已有:

keyversionprocessDefinitionIddeploymentId
asset_publish1asset_publish:1:10041001
asset_publish2asset_publish:2:15041501

再次部署相同 key 时,引擎通常生成 version 3。即使 XML 内容完全没改,只要没有启用并命中重复过滤,重复部署也可能产生新版本。

mermaid
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 提供:

java
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启动

java
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
        "asset_publish",
        assetId.toString(),
        variables);

引擎通常选择该 key 下最新的已激活定义。优点是业务代码不必保存最新 definitionId;风险是新定义一发布,新请求可能立即使用新版本。

9.2 按definitionId启动

java
ProcessInstance instance = runtimeService.startProcessInstanceById(
        approvedProcessDefinitionId,
        assetId.toString(),
        variables);

这能精确控制版本,适合灰度发布、租户分批切换或需要审批后才启用新定义的场景,但业务系统必须安全维护“当前允许启动的 definitionId”。

9.3 生产建议:业务发布指针

可以设计自己的流程发布配置表:

sql
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 查询最新版本。

mermaid
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。迁移前必须确认当前引擎版本能力并建立自动化测试。

原地迁移至少需要回答:

  1. 当前 Activity ID 在新定义中映射到哪个节点?
  2. 当前用户任务是保留、取消还是重建?
  3. 并行 Execution Token 怎样映射?
  4. 新定义必需变量怎样补齐?
  5. Timer、Job、Subscription 和 Boundary Event 怎样迁移?
  6. 历史表怎样标记迁移前后定义?
  7. 迁移中途失败怎样恢复?

回答不了这些问题时,优先让旧实例自然结束。

十二、流程定义缓存原理

引擎每次推进任务都重新解析数据库中的 BPMN XML,会造成大量 XML 解析和模型构建开销。因此流程定义通常有 Deployment Cache:

mermaid
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_BYTEARRAYACT_RE_PROCDEF 可能造成数据库与各节点缓存不一致。
  • 多节点集群中,不应依赖某个节点内存缓存判断“版本已全局生效”,应查询数据库定义并进行实际启动验证。

十三、挂起与激活定义

当发现新版本有问题但不想立刻删除审计资源时,可以考虑挂起定义。API 在不同版本上略有差异,常见形式如下:

java
repositoryService.suspendProcessDefinitionById(
        processDefinitionId,
        false,
        null);

第二个参数常用于决定是否同时挂起该定义下的运行实例,第三个参数用于延迟生效;请以当前版本方法签名为准。

恢复定义:

java
repositoryService.activateProcessDefinitionById(
        processDefinitionId,
        false,
        null);

必须区分:

  • 只挂起定义:阻止或影响新实例启动,已有实例是否受影响取决于 API 参数。
  • 同时挂起实例:已有任务推进也可能被阻止,业务影响更大。
  • 挂起不是回滚:它不会让新版本实例自动转成旧版本。
  • 激活旧定义不一定改变按 key 启动的最新版本选择,必须用实际查询/API 测试确认。

生产回退通常要同时处理“阻止错误版本新启动”和“把业务发布指针切回稳定 definitionId”。

十四、删除Deployment为什么危险

常见 API:

java
repositoryService.deleteDeployment(deploymentId, false);

cascade=false 时,如果该部署定义仍有关联运行实例,引擎通常拒绝删除。这种失败是在保护引用完整性。

级联删除:

java
repositoryService.deleteDeployment(deploymentId, true);

cascade=true 可能删除流程定义及其运行时、任务和历史相关数据,具体范围随版本和历史配置不同。它不是“强制下线新版本”的普通按钮,而是潜在的数据销毁操作。

mermaid
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 独立受控发布平台

适合多流程、多租户和高审计要求系统。平台负责模型仓库、校验、审批、部署、定义指针、灰度、回退和审计。

它治理能力最强,也引入额外系统复杂度。只有当流程数量、团队数量、变更频率或合规要求足够高时才值得建设。

十六、生产发布应该拆成哪些阶段

mermaid
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.3V3V2、V3riskLevel:String同时保留V2/V3调用的Bean
App 2.4V4V2、V3、V4新增可选dataOwnerId不删除旧方法签名

如果旧实例还在运行,而应用发布时删除了 V2 Service Task 使用的 JavaDelegate Bean,旧实例走到该节点时会失败。旧 BPMN 留在数据库不等于旧执行代码仍然存在。

生产发布前要统计旧版本运行实例:

java
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:

java
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 维度。
  • 公共模板和租户定制模板的继承/复制规则清晰。

十九、版本回退的准确做法

流程定义通常不是覆盖更新,因此“回退”并不一定要删除新版本。常见策略:

  1. 停止错误 definitionId 接受新实例,例如挂起或切换业务指针。
  2. 把新发起请求重新指向上一个稳定 definitionId。
  3. 统计已经进入错误版本的实例。
  4. 根据风险选择继续、补偿后重开或受控迁移。
  5. 保留错误定义和发布记录,便于审计与复盘。
mermaid
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 记录没有产生,或事务整体回滚。

排查顺序

mermaid
flowchart TD
    A["保存完整异常和根因链"] --> B["确认失败资源名和Git版本"]
    B --> C["独立解析XML并检查编码与命名空间"]
    C --> D["检查节点ID、连线引用和表达式"]
    D --> E["检查数据库连接、权限、锁和唯一约束"]
    E --> F["检查引擎版本与BPMN扩展兼容"]
    F --> G["修复后在测试环境重新部署并跑分支"]

不要只截取最外层 ActivitiException。XML 行列号、SQLState、Constraint 名称和最内层 Cause 才可能指向根因。

二十一、故障二:部署成功但查不到定义

先按 deploymentId 查询:

java
List<ProcessDefinition> definitions = repositoryService
        .createProcessDefinitionQuery()
        .deploymentId(deploymentId)
        .list();

检查:

  1. BPMN 中 <process isExecutable="true"> 是否正确。
  2. 查询的 tenantId、category、key 是否与部署一致。
  3. 一个 Deployment 是否包含多个定义,错误使用 singleResult()
  4. 应用是否连接了错误数据库或错误 schema。
  5. 事务是否尚未提交,另一个事务暂时不可见。
  6. 自动部署日志里的 deploymentId 是否属于当前环境。

SQL 只读核对示例:

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:

java
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
        .processInstanceBusinessKey(assetId.toString())
        .singleResult();

String actualDefinitionId = instance.getProcessDefinitionId();

再查定义:

java
ProcessDefinition actualDefinition = repositoryService
        .createProcessDefinitionQuery()
        .processDefinitionId(actualDefinitionId)
        .singleResult();

可能原因:

  • 业务代码按旧 definitionId 精确启动。
  • 业务发布指针或本地缓存没有切换。
  • 新定义处于 Suspended 状态。
  • tenantId 不同,查询的是另一个租户最新版本。
  • 实际请求进入了连接另一套数据库的旧应用实例。
  • 误以为运行中的旧实例应该自动采用新版本。

排查要记录“启动 API 参数 + 返回 processInstanceId + 实例 processDefinitionId + 定义 version”,不要只看流程名称。

二十三、故障四:旧实例发布后突然失败

最常见的不是 BPMN 资源消失,而是应用执行依赖不兼容:

  • 删除了旧 Service Task 使用的 Delegate Bean。
  • 修改了 Bean 方法签名或表达式名称。
  • 删除旧变量或把类型从 Boolean 改成 String。
  • 候选组规则变化后旧任务无人可见。
  • 数据库字段迁移不兼容旧流程路径。
  • Listener 新代码抛异常,导致任务完成事务回滚。

排查顺序:

  1. 查实例的 processDefinitionId 和 version。
  2. 下载该 Deployment 的原始 BPMN 资源。
  3. 定位当前 Activity/Task Definition Key。
  4. 对照当前应用中 Delegate、表达式和变量契约。
  5. 查看 Job、Incident/Dead Letter 能力对应的表与管理 API,具体因版本而异。
  6. 核对业务表是否因事务回滚保持原状态。

二十四、故障五:版本号异常增长

现象:应用每重启一次,version 就增加一次。

常见原因:

  • 每个实例启动时自动执行部署。
  • 没有启用重复过滤。
  • 生成 BPMN 时包含时间戳等非确定内容,每次字节都变化。
  • 多副本同时启动,各自执行自动部署。
  • 多个服务拥有同一 process key 的部署权。

治理:

  • 明确唯一发布者。
  • BPMN 制品保持确定性。
  • 在测试中验证重复部署行为。
  • 监控 key 的版本增长速率和部署来源。
  • 发布审计表记录应用实例、commit 和操作人。

不要通过直接删除中间版本“修正版本号”,版本号不连续通常不影响执行,破坏引用和历史才是真正风险。

二十五、故障六:删除定义失败

先判断是否仍有运行实例:

java
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和多个processsingleResult()异常或漏审定义
key就是definitionIdkey标识流程类型,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

级联删除可能同时移除定义、运行实例、任务和历史数据,范围取决于版本和历史配置。它会破坏业务关联和审计证据,尤其不适合作为新版本下线手段。应先统计运行实例、确认法规和历史保留要求,优先挂起定义或切换业务发布指针,历史清理走独立归档与审批流程。

三十、学习实验与验收

建议在本地测试数据库完成:

  1. 部署 V1,查询 Deployment、资源和 ProcessDefinition 三类记录。
  2. 启动实例 A,记录其 definitionId 和 businessKey。
  3. 修改任务名称但保持 key,部署 V2。
  4. 启动实例 B,证明 A 仍绑定 V1、B 绑定 V2。
  5. 查询两个版本运行实例数,验证兼容矩阵代码。
  6. 尝试非级联删除 V1 Deployment,观察运行实例引用保护。
  7. 挂起 V2,验证按 key 和按 ID 启动行为,以当前 Activiti 版本实测为准。
  8. 模拟业务发布指针切回 V1,验证新实例版本。
  9. 重启引擎进程,证明定义可从数据库重新加载而不是依赖旧内存缓存。
  10. 删除旧 Delegate Bean 的测试替身,证明 BPMN 资源存在仍不足以保证旧实例可执行。

验收时必须能回答:

  • 一次 Deployment 为什么可能生成多个 ProcessDefinition?
  • version、key、definitionId 和 deploymentId 各自解决什么身份问题?
  • 重复过滤能防什么,不能防什么?
  • 为什么旧实例不自动采用新定义?
  • 缓存丢失后模型怎样恢复?
  • 挂起、内容回退、实例迁移和级联删除有什么区别?
  • 新版本部署成功但业务仍走旧流程,证据从哪里取?
  • 为什么直接改 Activiti 表是一种高风险修复?

关联知识点