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

简介: 本文详解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 固化到业务配置和绑定表中,先部署再切流,最后暂停旧版的新建能力,可以把发布动作拆解为可观察、可验证、可回退的步骤。

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

相关文章
人工智能 缓存 前端开发
5897 14
人工智能 JavaScript 开发工具
2453 2
|
11天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
2027 121
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
缓存 JavaScript Shell
1022 1
|
12天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1577 13
|
9天前
|
编解码 弹性计算 云计算
MiniMax-H3 视频生成模型 — 一键部署与使用指南
MiniMax-H3是MiniMax开源的33B全模态视频生成模型,支持文生视频、图生视频、参考生视频三种模式,原生输出2K/15秒带立体声音频视频,已原生适配ComfyUI,并可通过阿里云计算巢一键部署。(239字)
缓存 人工智能 算法
572 0
|
18天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1979 10
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
10天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)