流程定义升级不影响在途实例:版本隔离、启动冻结与灰度发布实践

简介: 本文详解Flowable流程版本升级的可靠实践:通过“部署→切流→暂停”三步法,结合定义ID固化、业务绑定表与显式路由,实现新旧版本隔离、在途实例兼容及可审计回滚,避免版本混乱与数据错配。(239字)

审批流程上线后通常还会持续变化:增加复核节点、调整候选组、修改网关条件,或者为表单增加新的校验字段。直接部署新版 BPMN 文件看似简单,却容易产生三个问题:

  1. 新提交的单据究竟进入哪个版本,取决于调用方如何查询和启动流程;
  2. 已经运行到一半的实例不能被当作新版实例处理,否则节点结构和业务数据可能不匹配;
  3. 回滚应用代码时,如果流程定义没有同步控制,旧代码可能启动一个它无法正确处理的新流程。

可靠的升级方案需要把“定义发布”“新实例切流”和“在途实例处理”拆成三个独立动作。核心原则是:流程版本由引擎管理,业务选择由应用明确记录,在途实例默认留在原定义中运行。

本文示例采用 Flowable 的 Java API。不同发行版或项目依赖可能在自动配置属性、数据库脚本管理方式上存在差异,具体配置名应以项目实际依赖的官方文档和源码为准,但版本隔离与冻结定义 ID 的设计不依赖某个特定小版本。

先理解三个标识

Flowable 部署 BPMN 后,会形成流程定义。理解以下标识是避免版本混乱的前提:

  • processDefinitionKey:来自 BPMN 中 <process id="..."> 的逻辑名称,例如 expenseApproval。同一流程的多个版本共享这个 key。
  • processDefinitionId:某一次已部署定义的唯一标识。它对应确定的 key、版本和租户。
  • processInstanceId:一次实际运行的流程实例标识。

按 key 启动流程通常意味着由引擎选择符合条件的定义;按 definition ID 启动则明确绑定某个已部署版本。实例创建后会关联其流程定义,后续部署同 key 的新版本,并不会自动把已有实例改成新版本。

因此,发布 v2 后无需强制迁移 v1 的在途实例。除非业务确实要求迁移,而且已经验证了活动节点映射、变量兼容性、历史审计和补偿策略,否则迁移本身比并行运行风险更高。

设计一张业务绑定表

不要只在运行时临时查询“最新版本”。应在业务数据库中保存每张单据实际选择的定义和实例:

CREATE TABLE biz_workflow_binding (
    id BIGINT PRIMARY KEY,
    business_type VARCHAR(64) NOT NULL,
    business_key VARCHAR(128) NOT NULL,
    process_definition_id VARCHAR(128) NOT NULL,
    process_instance_id VARCHAR(128) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (business_type, business_key),
    UNIQUE (process_instance_id)
);

business_type 用于区分费用、采购等流程,business_key 对应业务单据号。唯一约束用于阻止重复提交造成多个流程实例。它不能替代引擎状态判断,但可以成为业务侧的幂等边界。

如果业务表和 Flowable 表使用同一数据源且由同一 Spring 本地事务管理,启动实例和写入绑定记录可以放在一个事务中。若两者位于不同数据库,则不能假设普通 @Transactional 可以提供跨库原子性,此时应采用本地状态机、事务消息或可重试的补偿流程。

步骤一:部署新定义,但暂不切流

将 v2 BPMN 使用新的资源名保存,例如 processes/expense-v2.bpmn20.xml。BPMN 中的 process key 保持不变,节点 ID 则应谨慎修改,因为业务监听器、表单和报表可能引用它们。

@Service
public class WorkflowReleaseService {
   
    private final RepositoryService repositoryService;

    public WorkflowReleaseService(RepositoryService repositoryService) {
   
        this.repositoryService = repositoryService;
    }

    public ProcessDefinition deploy(String resource, String releaseName) {
   
        Deployment deployment = repositoryService.createDeployment()
                .name(releaseName)
                .addClasspathResource(resource)
                .deploy();

        return repositoryService.createProcessDefinitionQuery()
                .deploymentId(deployment.getId())
                .singleResult();
    }
}

部署后先记录返回的 definition ID,不要立即让所有业务请求按 key 启动。发布检查至少包括:定义是否可查询、候选用户或候选组是否存在、表达式引用的变量是否有默认处理、服务任务委托类是否已经随应用发布。

生产环境不宜仅依赖应用启动时自动扫描 BPMN,因为多个实例同时启动可能让发布时点难以审计。更稳妥的方式是把部署做成受权限保护的发布动作,并记录操作人、制品校验值、deployment ID 和 definition ID。

步骤二:建立可控的版本路由

应用配置中保存当前允许创建新实例的 definition ID:

workflow:
  routes:
    expense: expenseApproval:7:8f3a-example

这里的值只是格式示意,实际 ID 必须使用部署后查询到的结果,不能从版本号自行拼接。绑定配置类:

@ConfigurationProperties(prefix = "workflow")
public record WorkflowProperties(Map<String, String> routes) {
   }

启动服务始终按 definition ID 创建实例:

@Service
public class ExpenseWorkflowService {
   
    private final RuntimeService runtimeService;
    private final RepositoryService repositoryService;
    private final WorkflowProperties properties;
    private final WorkflowBindingRepository bindingRepository;

    public ExpenseWorkflowService(
            RuntimeService runtimeService,
            RepositoryService repositoryService,
            WorkflowProperties properties,
            WorkflowBindingRepository bindingRepository) {
   
        this.runtimeService = runtimeService;
        this.repositoryService = repositoryService;
        this.properties = properties;
        this.bindingRepository = bindingRepository;
    }

    @Transactional
    public String submit(String orderNo, long amountFen) {
   
        String definitionId = properties.routes().get("expense");
        if (definitionId == null) {
   
            throw new IllegalStateException("未配置费用流程版本");
        }

        ProcessDefinition definition = repositoryService
                .createProcessDefinitionQuery()
                .processDefinitionId(definitionId)
                .active()
                .singleResult();
        if (definition == null) {
   
            throw new IllegalStateException("流程定义不存在或已暂停: " + definitionId);
        }

        Map<String, Object> variables = Map.of("amountFen", amountFen);
        ProcessInstance instance = runtimeService.startProcessInstanceById(
                definition.getId(), orderNo, variables);

        bindingRepository.insert(
                "expense", orderNo, definition.getId(), instance.getId());
        return instance.getId();
    }
}

金额使用整数分保存,避免流程表达式直接比较浮点数。网关条件所需变量应在启动前完成类型校验,不能让缺失变量直到流程运行时才暴露。

若需要灰度,可根据稳定且可复现的业务维度选择 v1 或 v2,例如组织、租户或业务类型。不要使用每次请求都变化的随机选择,否则同一单据重试时可能进入不同版本。无论采用何种规则,最终选中的 definition ID 都要持久化。

步骤三:切换新版并限制旧版新建

验证 v2 后,将路由配置从 v1 definition ID 切换为 v2。配置更新与应用实例生效可能不是瞬时完成的,因此启动接口仍需查询定义是否处于 active 状态,并对配置变更建立审计记录。

确认所有应用实例都已切换后,可以暂停旧定义,阻止它继续创建新实例,同时不暂停其已有实例:

public void stopNewInstancesOnOldVersion(String oldDefinitionId) {
   
    repositoryService.suspendProcessDefinitionById(
            oldDefinitionId,
            false,
            null);
}

第二个参数为 false 表示不连带暂停已有流程实例。调用前仍应在当前项目使用的 Flowable 依赖上做集成验证,尤其要覆盖定时器、异步作业和外部任务等项目实际使用的能力。

操作顺序不能颠倒:如果先暂停 v1,而部分应用仍持有 v1 配置,这些请求会直接失败。推荐顺序是部署 v2、验证 v2、更新路由、确认配置收敛、最后暂停 v1 的新建能力。

在途实例如何保持兼容

旧实例可以继续使用 v1 定义,但应用代码也必须兼容它。常见做法包括:

  • 读取绑定表中的 definition ID,再决定展示哪套表单字段;
  • 任务处理根据 taskDefinitionKey 分派,不把所有版本强行映射到新版节点;
  • 新增变量时提供缺省值,不假设旧实例已经拥有该变量;
  • 服务任务委托类保持向后兼容,或用独立类名承载新版逻辑;
  • 删除旧代码前,查询旧定义下是否仍有运行实例和未完成任务。

可以按定义 ID 查询在途数量:

long running = runtimeService.createProcessInstanceQuery()
        .processDefinitionId(oldDefinitionId)
        .count();

只有计数归零还不一定代表可以立刻清理。若系统依赖历史查询、审计报表或流程图展示,删除部署可能影响历史资料可用性。是否删除旧部署,应根据历史级别、归档策略和合规要求单独评估;通常保留已完成版本比立即删除更容易审计。

回滚方案

如果 v2 在切流后出现问题,可执行以下步骤:

  1. 停止继续扩大流量,将业务路由改回仍可用的 v1 definition ID;
  2. 如 v1 已暂停,先恢复该流程定义,再恢复业务入口;
  3. 暂停 v2 的新实例创建,但默认不要连带暂停已经创建的 v2 实例;
  4. 按绑定表找出切流期间创建的 v2 实例,逐单决定继续、撤销还是人工补偿;
  5. 修复后部署新的定义版本,不要直接篡改已经部署的流程资源或引擎表。

恢复定义可使用:

repositoryService.activateProcessDefinitionById(
        oldDefinitionId,
        false,
        null);

回滚应用代码之前,还必须确认旧代码能否处理已经存在的 v2 任务。流程路由回滚只能影响后续新实例,不能自动把 v2 在途实例变回 v1。

常见问题

为什么不直接调用 startProcessInstanceByKey

按 key 启动适合版本策略简单、部署流程严格受控的系统。但在多实例滚动发布、灰度或紧急回滚场景中,它会把版本选择隐含在引擎查询规则中。按 definition ID 启动并落库,版本决策更容易复现和审计。

暂停旧定义会不会让旧任务无法审批?

取决于是否连带暂停实例。本文方案暂停定义时传入 false,目标是禁止以旧定义创建新实例,同时让既有实例继续运行。上线前应通过集成测试验证项目使用的监听器、作业执行器和任务操作。

能否把 v1 在途实例全部迁到 v2?

技术上可以设计迁移,但不能仅凭节点名称相似就执行。至少需要明确活动节点映射、并行分支、子流程、边界事件、变量转换、定时器以及历史审计的处理方式。若没有强制业务需求,让旧实例自然结束通常更可控。

唯一约束冲突后可以直接重试启动吗?

不能盲目重试。应先按 business_type + business_key 查询绑定记录,确认实例是否已经创建。跨数据库场景还要处理“引擎已启动但业务绑定未写入”或相反状态,通过对账任务修复,而不是重复创建实例。

如何测试升级流程?

集成测试应至少覆盖:v1 实例启动后部署 v2,v1 任务仍可完成;切流后新单据使用 v2;暂停 v1 后按 v1 ID 启动失败但原实例可继续;路由回退后新实例重新进入 v1;同一业务单号并发提交时只产生一条有效绑定。

总结

流程改版的关键不是“最新版本能否部署”,而是版本选择是否显式、在途实例是否隔离、回滚是否只影响预期范围。将 definition ID 固化到业务配置和绑定表中,先部署再切流,最后暂停旧版的新建能力,可以把发布动作拆解为可观察、可验证、可回退的步骤。

对于普通节点调整,优先让不同版本并行并让旧实例自然结束;只有在法规、业务规则或严重缺陷要求下,才为在途实例设计专门迁移。流程引擎负责保存版本关系,应用则必须对“哪个业务进入哪个版本”承担明确责任。

相关文章
|
23天前
|
数据采集 人工智能 算法
45条AI引用源实测:内容平台权重分布与信息块拆解
本文拆解豆包AI的45条引用源,揭示CSDN、头条、搜狐占国内引用近半;剖析被高频引用的CSDN文章结构参数(如数字密度、列表数、H2标题),提出“平台推荐→AI抓取→被引用”链路及可复现的监测方法。
160 1
|
22天前
|
开发工具 Swift git
DeepSeek Harness 插件推荐:4 款开源神器让写代码直接起飞
DeepSeek Harness 插件推荐:ModLens 视觉、Web UI 全家桶、Mac 原生与 GenUI 渲染,4 款开源插件给纯文本模型补齐短板。
2195 7
DeepSeek Harness 插件推荐:4 款开源神器让写代码直接起飞
|
22天前
|
Kubernetes 应用服务中间件 nginx
Kubernetes (K8s) 从入门到实战:命名空间、Pod、Controller、Service,图文并茂
本文是K8s初学者的实战笔记,系统讲解命名空间(隔离资源、环境划分、权限控制)、Pod(最小调度单元、多容器、标签、生命周期、探针、资源限制)、控制器(Deployment灰度发布/回滚、StatefulSet/Job/DaemonSet)及Service(ClusterIP/NodePort、负载均衡、多端口)等核心概念与操作,附带丰富命令示例和原理剖析。
Kubernetes (K8s) 从入门到实战:命名空间、Pod、Controller、Service,图文并茂
|
22天前
|
Ubuntu 测试技术 网络安全
【Docker项目实战篇】Docker部署PDD查看器PdfDing
【Docker项目实战篇】Docker部署PDD查看器PdfDing
153 2
【Docker项目实战篇】Docker部署PDD查看器PdfDing
|
22天前
|
安全 Linux
Linux系统之gzip命令的基本使用
Linux系统之gzip命令的基本使用
102 1
Linux系统之gzip命令的基本使用
|
23天前
|
文件存储 虚拟化 数据安全/隐私保护
ESXI服务器备份工具docker版
YueBackup 是一款免费开源的 ESXi 虚拟机备份工具,提供 Web 管理界面,支持手动/定时备份、一键还原、多主机管理,无需虚拟机内安装 Agent,Docker 一键部署,适用于家庭及中小企业低成本灾备需求。(239字)
145 1
ESXI服务器备份工具docker版
|
2月前
|
缓存 Java 数据库连接
[053][核心模块]Java枚举缓存与ORM集成实践
本文介绍Java枚举缓存与ORM(MyBatis/JPA)的通用集成方案:通过`EnumCache`双向哈希缓存(O(1)查找)、`BaseEnum`统一接口及自动注册的类型转换器,解决枚举查值性能低、重复编码、缓存不一致等痛点,提升可维护性与运行效率。(239字)
168 3
|
22天前
|
人工智能 程序员 开发工具
AI时代程序员需要关注的点(个人理解)
AI时代,经过我自己原创的项目,十几天的不断产品迭代,是对自己所有经验以及与AI结合,不断发现问题,不断要求AI去改,不断AI改,我不断抽象的过程,以及我不断精进的过程。
56 2
|
21天前
|
运维 分布式数据库 数据库
分布式数据库在线扩容首选:阿里云PolarDB-X不停机Rebalance实战
实测数据表明,PolarDB-X在扩容期间的业务影响显著优于TiDB和OceanBase:TPS降幅低于1%(优于TiDB的5-10%),P99延迟增幅低于5%(优于TiDB的15-30%)。对于金融交易等对延迟敏感的场景,PolarDB-X是在线扩容的最优选。 适用于金融核心交易系统、证券实时行情系统、支付清算平台等对业务连续性要求极高的场景。也适用于电商大促前的容量储备、SaaS平台多租户扩展等需要快速弹性扩容的场景。
80 1

热门文章

最新文章