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"
}
}
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 | 最后更新时间 |
4.4 整体架构图
下面这张图把节点、边、状态转移和存储的关系串起来了。你仔细看,其实核心就三个圈:定义、实例、执行引擎。
4.5 状态转移的完整流程
嗯,这里要重点说一下。状态转移不是简单的「走完A到B」,它包含几个关键步骤:
- 节点执行完成:当前节点返回结果(成功/失败/超时)
- 收集出边:从流程定义中找出所有从当前节点出发的边
- 条件评估:按优先级依次评估每条边的条件表达式
- 选择路径:选中第一条条件为真的边
- 更新实例:将实例的 current_node 更新为目标节点
- 持久化上下文:保存当前上下文到数据库
核心原则:状态转移必须是「原子操作」。要么全部成功,要么回滚到上一个节点。我见过因为转移一半数据库挂了,导致实例状态不一致的惨案。
我曾经在一个金融项目里,就因为没做转移的原子性,结果用户重复扣了两次款。从那以后,我强制要求所有状态转移必须在一个数据库事务里完成。
4.6 避坑指南
- 别把节点设计得太细:一个节点只做一件事。我见过有人把「查数据库+发短信+记日志」塞一个节点里,出了问题根本定位不了。
- 边的条件别写太复杂:超过三个条件的,拆成多个判断节点。否则你维护时会想骂人。
- 上下文变量要有命名规范:比如统一用
ctx.userId、ctx.orderAmount,别一会儿 snake_case 一会儿 camelCase。 - 版本控制不能省:流程定义每次修改都要升版本,线上跑的实例用旧版本,新实例用新版本。不然你发布一次,线上全崩。
好了,行为流建模的核心就这些。节点是骨架,边是血管,存储是记忆。把这三样搞清楚了,后面的规则特征联动才能跑得稳。