4. 行为流建模:节点、边与存储结构

行为流建模,说白了就是把业务里的「动作」和「状态变化」变成计算机能理解的结构。我做了这么多年规则引擎,发现很多团队卡在这一步——要么把节点设计得太重,要么把状态转移搞成了面条代码。

今天咱们就拆开揉碎了讲。从节点设计开始,到边与状态转移,最后落到存储结构上。

4.1 节点设计:行为的最小单元

节点是行为流的基本积木。每个节点代表一个「可执行的步骤」。我个人习惯把节点分成三类:

节点类型 职责 典型场景
动作节点 执行一个具体操作 发送短信、调用API、更新数据库
判断节点 根据条件选择分支 用户等级判断、金额阈值检查
事件节点 等待外部事件触发 用户点击、超时、消息到达

每个节点内部,我建议至少包含这几个字段:

{
  "nodeId": "node_001",
  "type": "action",          // action | decision | event
  "name": "发送验证短信",
  "config": {
    "provider": "aliyun",
    "templateId": "SMS_12345"
  },
  "timeout": 5000,           // 毫秒
  "retryPolicy": {
    "maxRetries": 3,
    "backoff": "exponential"
  }
}
我的经验:节点ID最好用业务语义命名,比如 send_sms_node,别用纯数字。我在项目里见过全用自增ID的,调试时根本分不清哪个是哪个。

4.2 边与状态转移:行为怎么走

节点之间靠「边」连接。边定义了行为流的走向。每条边包含三个要素:

  • 源节点:从哪出发
  • 目标节点:到哪去
  • 条件表达式:什么情况下走这条边

举个例子,一个简单的风控流程:

// 边的数据结构
{
  "edgeId": "edge_001",
  "from": "check_user_node",
  "to": "send_alert_node",
  "condition": "riskScore > 80",
  "priority": 1
}

状态转移的核心逻辑其实就一句话:当前节点执行完后,根据边的条件,选择下一条可用的边。

关键点:边的条件表达式必须是「纯函数」——不依赖外部状态,只依赖当前上下文。否则你调试时会疯掉。

我曾经遇到过一个线上事故:某条边的条件里偷偷调了个外部API,结果API挂了,整个行为流卡死在那。从那以后,我强制团队所有条件表达式只能用上下文变量,不能有副作用。

4.3 行为流的存储结构

存储结构决定了行为流的「可追溯性」和「可恢复性」。我推荐用两张表来存:

4.3.1 流程定义表

存的是「模板」——行为流长什么样,节点和边怎么连。

字段 类型 说明
flow_id varchar(64) 流程唯一标识
version int 版本号,每次修改+1
nodes json 节点列表
edges json 边列表
status tinyint 0=草稿, 1=已发布, 2=已废弃

4.3.2 流程实例表

存的是「运行记录」——每个用户走到哪了,上下文是什么。

字段 类型 说明
instance_id varchar(64) 实例唯一标识
flow_id varchar(64) 关联的流程定义
current_node varchar(64) 当前所在节点ID
context json 运行时上下文变量
status tinyint 0=运行中, 1=完成, 2=异常, 3=超时
created_at datetime 创建时间
updated_at datetime 最后更新时间
注意:context字段别存太大。我见过有人把整个请求体都塞进去,结果一张表几十GB。只存行为流需要的变量,其他放外部存储。

4.4 整体架构图

下面这张图把节点、边、状态转移和存储的关系串起来了。你仔细看,其实核心就三个圈:定义、实例、执行引擎。

行为流建模核心架构 流程定义 节点列表 边列表 版本控制 流程实例 当前节点 上下文变量 运行状态 执行引擎 节点调度 条件评估 状态转移 实例化 驱动 节点类型 动作节点 · 判断节点 · 事件节点 每个节点有独立配置和超时策略 边与转移 源节点 → 目标节点 条件表达式 + 优先级 存储结构 流程定义表(模板) 流程实例表(运行记录) 三者联动:定义 → 实例 → 引擎 → 状态转移 每次转移都记录到实例表中,保证可追溯

4.5 状态转移的完整流程

嗯,这里要重点说一下。状态转移不是简单的「走完A到B」,它包含几个关键步骤:

  1. 节点执行完成:当前节点返回结果(成功/失败/超时)
  2. 收集出边:从流程定义中找出所有从当前节点出发的边
  3. 条件评估:按优先级依次评估每条边的条件表达式
  4. 选择路径:选中第一条条件为真的边
  5. 更新实例:将实例的 current_node 更新为目标节点
  6. 持久化上下文:保存当前上下文到数据库

核心原则:状态转移必须是「原子操作」。要么全部成功,要么回滚到上一个节点。我见过因为转移一半数据库挂了,导致实例状态不一致的惨案。

我曾经在一个金融项目里,就因为没做转移的原子性,结果用户重复扣了两次款。从那以后,我强制要求所有状态转移必须在一个数据库事务里完成。

4.6 避坑指南

  • 别把节点设计得太细:一个节点只做一件事。我见过有人把「查数据库+发短信+记日志」塞一个节点里,出了问题根本定位不了。
  • 边的条件别写太复杂:超过三个条件的,拆成多个判断节点。否则你维护时会想骂人。
  • 上下文变量要有命名规范:比如统一用 ctx.userId、ctx.orderAmount,别一会儿 snake_case 一会儿 camelCase。
  • 版本控制不能省:流程定义每次修改都要升版本,线上跑的实例用旧版本,新实例用新版本。不然你发布一次,线上全崩。

好了,行为流建模的核心就这些。节点是骨架,边是血管,存储是记忆。把这三样搞清楚了,后面的规则特征联动才能跑得稳。