改了工作流引擎的流程定义,在跑的审批单有的跟着变、有的不变

做过工作流引擎的,都遇到过这么一个瞬间:

周一把审批流程从"两级审批"改成"三级审批",发布了。周二早上,业务同学问:昨天那 20 张还在审批中的单子,现在走两级还是三级?

如果你的引擎回答不了这个问题,或者回答得很随意------那多半是设计时就没想过"定义"和"正在用这份定义跑着的实例"是两回事。

这篇文章做的事,就是把这个问题在 jeeflow 里拆到底:一条在跑的审批单,它到底"记着"流程定义的什么;改了定义之后,下一次它走到引擎时,引擎读到的到底是新版还是旧版。拆完你会发现一个反直觉的结论------答案不是"都走新版",也不是"都走旧版",而是取决于你当时点的是哪个按钮

一、先分清两个东西:设计稿,和流程定义

jeeflow 里,"一份流程"在数据库里其实分住在两张表里,对应两个完全不同的东西:

  • 设计稿(草稿)wf_process_design + wf_process_design_his。设计器里画的东西、改了又改的东西,都存在这里。his 表是快照历史,你每保存一次就留一个版本。
  • 流程定义(发布产物)wf_process_define。引擎真正拿去跑审批的东西,带一个 version 列。

规范里这句话是硬约定(06-facade.md):

设计稿(草稿)≠ 定义(发布产物):save/updateDefine 存草稿快照(历史表),deploy 才生成流程定义。设计稿内容变更后 isDeployed 自动置 0(防误用旧定义)。

翻译成人话:你在设计器里画得再欢,不点"发布",引擎一无所知。 而且你一旦在设计器里改了内容,那份"已发布"的标记会自动被抹掉------这是防"你以为改了已经生效"的护栏,isDeployed=0 意味着"当前草稿和线上跑的那版对不上了"。

java 复制代码
// JeeflowFacade.java:897  processDesign/updateDefine
// 内容快照变更 → 置为未部署(对齐 boot3 updateDefine 语义)
if (contentBytes(args) != null) design.setIsDeployed(0);

所以"设计稿 → 定义"是一次发布动作,不是自动同步。这一步是后文所有版本行为的分界。

二、两个按钮,两种改法:deployredeploy

发布有两条路,它们对"版本号"和"定义 id"的处理不一样,这正是后文在跑的单子走向不同的根源。

deploy:发一个新版本,拿到一个新 id

deploy 的语义是"按流程名(name)找最新那条定义,有就把版本号 +1 插一条新记录,没有就从 0 起":

java 复制代码
// JeeflowFacade.java:1202  deploy 版本管理(对齐 boot3)
private Long saveDeployedDefine(ProcessModel model, byte[] bytes) {
    // 按 name 查最新定义
    PageResult<...> page = repository.pageDefines(query);
    int version = 0;
    if (page.getRows() != null && !page.getRows().isEmpty()) {
        Integer latest = page.getRows().get(0).getVersion();
        version = (latest != null ? latest : 0) + 1;   // ← version+1
    }
    def.setVersion(version);
    repository.saveDefine(def);        // ← 插一条新记录,新 id
    return def.getId();                // ← 返回新 id
}

Go 侧同一语义(facade.go:275):version = latest.Version + 1

关键在最后一行:deploy 返回的是一条新记录的 id 。旧记录还在库里躺着(所以 wf_process_define 表里同一个 name 会有 v0、v1、v2... 多行),新版本是一个全新的 id

redeploy:原地改内容,id 和版本号都不动

redeploy(重新发布)的语义完全反过来------它不换行,就在原来那条记录 上把内容(content)覆盖掉,版本号保持原样:

java 复制代码
// JeeflowFacade.java:1005  designRedeploy 按 name 取最新定义
IProcessRepository.DefineRow last = page.getRows().get(0);
ProcessInstance.ProcessDefine def = new ProcessInstance.ProcessDefine();
def.setId(last.getId());          // ← 还是原来那条 id
def.setContent(bytes);            // ← 只覆盖内容
// issues/59:保留原 version(替换语义,不递增)
def.setVersion(last.getVersion());
repository.updateDefine(def);     // ← UPDATE 原记录,不插新行

这里那行注释值得单独看:issues/59。这是引擎里一个真实修过的 bug------redeploy 本该"原地替换、版本号不变",但某个版本漏设了 version 字段,被 JDBC 兜底逻辑误写成了 1,导致一次原地重发把版本号从 3 打回了 1。这个 bug 本身说明一件事:redeploy 的设计意图是"换内容、不换身份",版本号和 id 都是"身份"的一部分,不能被顺带改掉。

两个按钮一句话对比:

deploy redeploy
记录 插新行 覆盖原行
版本号 +1 不变
定义 id 新的 不变
语义 发新版本 热改当前版内容

三、一条在跑的单子,"记着"流程定义的什么

要回答"在跑的单子走哪版",得先知道它记了定义的什么。

答案有点反直觉:它不记版本号,只记 id。 看实例表的建表语句(规范 01-data-model.md,各语言建表都从这一份派生):

sql 复制代码
CREATE TABLE wf_process_instance (
    id                  BIGINT PRIMARY KEY,
    process_define_id   BIGINT NOT NULL COMMENT '流程定义ID',   -- 只有 id
    state               INT DEFAULT 10,
    ...
) COMMENT='流程实例表';

对比一下定义表,version 列只在 wf_process_define 上:

sql 复制代码
CREATE TABLE wf_process_define (
    id      BIGINT PRIMARY KEY,
    name    VARCHAR(100) NOT NULL,
    content BLOB,
    version INT DEFAULT 0,     -- 版本号只在定义表
    ...
);

实例表里压根没有 version 这一列。 一条在跑的审批单,跟流程定义的全部关系就是 process_define_id 这一个外键。它不快照版本号,不快照流程内容------它只记"我这条单是按哪条定义发起的"。

那引擎执行的时候拿什么跑?答案是:每次执行,都按这个 id 现查定义、现解析。 Java 引擎在发起时(JeeflowEngineImpl:72):

java 复制代码
ProcessInstance.ProcessDefine define = repository.findDefineById(defineId);
ProcessModel model = ModelParser.parse(define.getContent());   // 解析内容

而在每一次办理任务 时(JeeflowEngineImpl:244,这才是关键的循环):

java 复制代码
ProcessInstance instance = repository.findInstanceById(task.getProcessInstanceId());
ProcessInstance.ProcessDefine define = repository.findDefineById(instance.getDefineId());
ProcessModel model = ModelParser.parse(define.getContent());

注意是 findDefineById(instance.getDefineId())------按 id 查,不是按"我发起时那个版本"查 。Go 引擎一模一样(engine_impl.go:257):

go 复制代码
def, _ := e.repo.FindDefineByID(ctx, inst.DefineID)

把这两条事实放在一起,结论就自己浮出来了。

四、所以,在跑的单子到底走哪一版

引擎每次执行都按实例记的那个 define_id 现查定义内容------那么"走哪一版"完全取决于:你改定义的动作,有没有换掉这个 id

  • 你用 redeploy 改了流程 :id 不变、版本号不变,只有 content 变了。在跑的单子记的 id 没变,但引擎下一次按这个 id 现查,读到的是你刚改过的内容 。→ 在跑的单子跟着变。
  • 你用 deploy 发了新版本 :新记录、新 id、版本号 +1。在跑的单子记的还是旧 id ,引擎按旧 id 查,读到的还是旧内容。→ 在跑的单子不受影响,走旧版。

用开头那个场景收个尾:周一你把"两级审批"改成"三级审批"------

  • 若你走的是 redeploy(原地改那一条):昨天那 20 张在跑的单子,今天批到下一步时,会开始按三级审批走。业务同学昨天看到的待办,今天多了一个审批环节。
  • 若你走的是 deploy(发 v1 新版本):昨天那 20 张单子继续按两级审批跑完,只有今天之后新发起的单子才走三级。

同一次改动,两个按钮,在跑的单子走向相反。 这就是"有的跟着变、有的不变"的完整机理,而且它不是 bug------是"引擎按 id 现查定义"这个设计 + "两个按钮对 id 的不同处理"叠加出来的必然结果。

五、redeploy 是能力,也是刀口

上面的结论顺带回答了一个更尖锐的问题:redeploy 让"改流程不用等单子跑完"成了可能------这既是工作流引擎里很受欢迎的热更能力,也是一把刀口。

它危险的地方在于:你 redeploy 改完内容的那一刻,所有钉在这个 id 上的在跑实例,下一次执行都会读到新结构。如果你的新结构把某个审批节点删了、把审批人换了、把会签比例改了------那些正在半路的单子会按新规则继续走。有的场景你正想要这个效果(线上紧急加一个审批人),有的场景这是事故(在跑的报销单突然多了个环节,财务懵了)。

反过来,deploy 发新版本因为换了 id,天然把"在跑的"和"新发起的"隔开了------老单子安安稳稳按老版跑完,新单子走新版。这是更接近"发布 = 不可变快照"的心智模型,代价是你无法热改在跑的单子。

两个按钮其实对应两种工程取舍,选哪个取决于你对"改动影响面"的偏好:

  • 想让改动立刻作用到存量在跑的单子 (热更)→ redeploy,但要清楚你在改所有存量的运行轨迹。
  • 想让改动只影响新发起的单子 (版本隔离)→ deploy,老单子按老版自然走完。

六、还有两件事,容易被忽略

1. "按名字取最新"是另一条隐线。 引擎里多处用 getLastByName(按流程名取最新版本)来决定"新发起的单子用哪条定义"。也就是说,发起路径认的是"这个名字的最新定义",而运行路径认的是"这条实例钉住的那个 id"。这两条线在大多数时候指向同一条记录(最新且未被替换),但一旦你 deploy 出新版本,发起会指到新 id、存量还在跑旧 id------"新发起的"和"在跑的"从此分叉,这正是上一节说的版本隔离的由来。

2. 定义被禁用(upAndDown state=0)会拦住发起,但不拦在跑的。 processDefine/upAndDown 把定义 state 置 0 后,前端"发起申请"页靠 listByType 返回的 processDefineState 控制发起按钮(state=0 时不可发起)------这是发起路径的硬依赖。但一条已经发起、正在跑的实例,它的运行路径是按 id 现查定义内容来执行的,并不会因为定义被停用而停下。所以"停用"拦的是新单,不是老单------这也是同一个"发起认 name、运行认 id"设计的延伸。

写在最后

把这条链捋一遍:

bash 复制代码
设计稿(his 快照)
   └─ deploy ──→ 定义(新行、version+1、新 id)
   └─ redeploy ─→ 定义(原行、version 不变、内容覆盖)
                          │
        实例只记 process_define_id(不记 version)
                          │
        引擎每次执行:findDefineById(实例.id) 现查现解析
                          │
   redeploy 改过 → 在跑的单子跟着变
   deploy 发新版 → 在跑的单子走旧版

一条在跑的审批单,它记的永远只是"我按哪条定义发起的"这一个 id;而引擎每次执行都按这个 id 现查内容。所以改了定义之后它走哪一版,不由"版本号"决定,而由"你的改动有没有换掉这个 id"决定 ------redeploy 换内容不换 id,所以在跑的单子跟着变;deploy 换新 id,所以在跑的单子被隔在旧版里。

如果你的引擎被问"改了流程,在跑的单子走哪版"时答不上来,多半是"定义"和"实例"在它的模型里还没分开------这条 process_define_id 外键,就是那个分界的物理体现。

参考资料(往期相关篇目与链接):

相关推荐
JavaGuide1 小时前
为什么越来越多人放弃 Claude Code 转而用 Pi?
前端·后端
ShadowYD7 小时前
Agent Loop:一个让 AI 编程 Agent 从"会写代码"进化到"闭环交付"的开源 Skill(适配国内外主流 Code Agent)
后端
考虑考虑8 小时前
nohup启动java程序
后端·自动化运维
aramae10 小时前
模拟实现strcmp()(C语言)
c语言·开发语言·后端
根目录下的猫12 小时前
RK3588适配的轻量级AI模型推荐
人工智能·后端·python·目标检测
星栈13 小时前
用 Rust 写 Agent 服务 -- adk-rust 上手记
后端·agent
杨运交13 小时前
[071][验证码模块]基于Spring拦截器的验证码认证设计思想
java·后端·spring