编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

    • README
    • 架构与组件

    • 数据与存储

    • 通信与协议

    • 稳定性与安全

    • 端侧专项性

    • 研发的效能

      • 通用轮训方案设计
      • 状态机设计的思想
        • 01.已发货订单又被支付
          • 1.1 一次状态错乱事故
          • 1.2 事故扩散链路
          • 1.3 反思状态管理
        • 02.状态机是什么
          • 2.1 一句话定义
          • 2.2 五元组数学模型
          • 2.3 状态机的类型
          • 2.4 状态机六大好处
        • 03.要解决的核心矛盾
          • 3.1 状态分散与一致
          • 3.2 灵活与约束
          • 3.3 简单与扩展
          • 3.4 状态机的本质
        • 04.业界主流方案
          • 4.1 主流实现方式
          • 4.2 横向对比矩阵
          • 4.3 选型速查
        • 05.设计核心原则
          • 5.1 状态封闭原则
          • 5.2 转换显式原则
          • 5.3 默认拒绝原则
          • 5.4 副作用隔离
        • 06.从普通代码到状态机的演变
          • 6.1 V0 散落式 if-else
          • 6.2 V1 集中式状态判断
          • 6.3 V2 转换表 + 状态机
          • 6.4 V3 配置化 DSL
          • 6.5 演变路径全景
        • 07.方案落地实战
          • 7.1 整体架构
          • 7.2 五元组建模
          • 7.3 状态机重构核心
          • 7.4 转换流程
        • 08.关键问题解决
          • 8.1 状态爆炸
          • 8.2 状态持久化
          • 8.3 并发与幂等
        • 09.常见陷阱与反例
          • 9.1 状态散布反例
          • 9.2 副作用混入反例
          • 9.3 过度设计反面教材
          • 9.4 何时不要用状态机
        • 10.总结与决策
          • 10.1 上线检查表
          • 10.2 选型决策树
      • 通用搜索方案设计
      • 通用权限模型方案
  • 专栏
  • 方案设计思想
  • 研发的效能
杨充
2026-05-21
目录

状态机设计的思想

# 28.状态机设计的思想

本篇定位:状态机是后端最朴素也最强大的"业务建模武器",但绝大多数业务代码都把它写成了"if-else 地狱"。本文从一次"已发货订单又被支付一次"的事故讲起,回答三个核心问题,状态机到底解决什么本质问题?什么时候用、什么时候不用?怎么落地一套扛得住业务变化的状态机?

# 目录介绍

  • 01.已发货订单又被支付
    • 1.1 一次状态错乱事故
    • 1.2 事故扩散链路
    • 1.3 反思状态管理
  • 02.状态机是什么
    • 2.1 一句话定义
    • 2.2 五元组数学模型
    • 2.3 状态机的类型
    • 2.4 状态机六大好处
  • 03.要解决的核心矛盾
    • 3.1 状态分散与一致
    • 3.2 灵活与约束
    • 3.3 简单与扩展
    • 3.4 状态机的本质
  • 04.业界主流方案
    • 4.1 主流实现方式
    • 4.2 横向对比矩阵
    • 4.3 选型速查
  • 05.设计核心原则
    • 5.1 状态封闭原则
    • 5.2 转换显式原则
    • 5.3 默认拒绝原则
    • 5.4 副作用隔离
  • 06.从普通代码到状态机的演变
    • 6.1 V0 散落式 if-else
    • 6.2 V1 集中式状态判断
    • 6.3 V2 转换表 + 状态机
    • 6.4 V3 配置化 DSL
    • 6.5 演变路径全景
  • 07.方案落地实战
    • 7.1 整体架构
    • 7.2 五元组建模
    • 7.3 状态机重构核心
    • 7.4 转换流程
  • 08.关键问题解决
    • 8.1 状态爆炸
    • 8.2 状态持久化
    • 8.3 并发与幂等
  • 09.常见陷阱与反例
    • 9.1 状态散布反例
    • 9.2 副作用混入反例
    • 9.3 过度设计反面教材
    • 9.4 何时不要用状态机
  • 10.总结与决策
    • 10.1 上线检查表
    • 10.2 选型决策树

# 01.已发货订单又被支付

# 1.1 一次状态错乱事故

某电商大促,运营紧急上线一个"补偿支付"接口,用户支付被风控误拦时,可以重新调用补偿。结果上线第二天大事故:

  • 客服收到大量投诉:"我已经收到货了,怎么又扣了一次钱?"
  • 财务系统对账:1.7 万笔订单被重复支付,总金额 230 万
  • 紧急排查:补偿支付接口只校验"订单存在 + 金额匹配",没校验"当前状态"
  • 已发货 / 已收货状态的订单也能被"补偿支付",直接二次扣款
gantt
    title 重复支付事故时间线
    dateFormat HH:mm
    axisFormat %H:%M
    
    section 上线
    补偿支付接口上线  :a, 09:00, 1h
    
    section 灾难
    1.7w 笔重复扣款  :crit, b, 10:00, 8h
    客服炸锅          :crit, c, 11:00, 7h
    
    section 复盘
    紧急下线接口     :crit, d, 18:00, 1h
    全量退款 230w    :crit, e, 19:00, 12h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 1.2 事故扩散链路

flowchart TD
    A[订单状态分散在 30 个地方] --> B[补偿支付只校验金额]
    B --> C[已发货订单被二次支付]
    C --> D[1.7w 笔重复扣款]
    D --> E[客服 / 财务 / 信任全崩]
    
    Cause[根因] --> R1[状态判断散落各处<br/>每处都在 if order.status]
    Cause --> R2[新接口忘了写状态校验]
    Cause --> R3[没有统一的 状态转换约束]
    Cause --> R4[非法转换无人拦截]
    
    style D fill:#ffebee
    style Cause fill:#fff3e0
1
2
3
4
5
6
7
8
9
10
11
12
13

# 1.3 反思状态管理

事后这个团队总结了三个最深刻的教训:

  1. 状态判断必须收敛到一处,分散在 30 个 if-else 里 = 30 个潜在 bug
  2. 转换约束必须代码化,不能靠"开发记得校验"
  3. 非法转换必须默认拒绝,而不是默认允许

状态机的真正价值不是"枚举几个状态",而是 "用代码把业务规则变成不可绕过的约束"。

# 02.状态机是什么

# 2.1 一句话定义

状态机(State Machine)= 在任意时刻,系统只能处于一个明确状态;状态之间的切换由事件触发,并严格遵循预定义规则。

用大白话说,状态机就是一个 "智能开关":

  • 它记得自己当前在哪个状态
  • 它根据收到的事件 + 当前状态,决定要不要切换、切换到哪
  • 它拒绝所有不在规则表里的非法切换

以网购订单为例:"待支付 → 已支付 → 已发货 → 已收货 → 已完成",每一步切换都由具体事件(支付/发货/收货)触发,且只能按预定路径走。

# 2.2 五元组数学模型

学术上状态机被定义为一个五元组 (S, Σ, δ, s₀, F):

graph LR
    subgraph "状态机五元组"
        S[S: 状态集合<br/>所有可能的状态]
        E[Σ: 事件集合<br/>所有可触发的输入]
        D[δ: 转换函数<br/>S × Σ → S]
        S0[s₀: 初始状态]
        F[F: 终态集合]
    end
    
    S --> D
    E --> D
    D --> Next[下一状态]
    
    style D fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14

工程视角的等价表达:

数学符号 工程概念 订单举例
S 状态枚举 PENDING / PAID / SHIPPED / ...
Σ 事件枚举 PAY / SHIP / CANCEL / ...
δ 转换规则表 (PENDING, PAY) → PAID
s₀ 初始状态 PENDING
F 终态 COMPLETED / CANCELLED

再加上工程实践的两个补充:

  • 守卫(Guard):转换的前置条件,例如"金额必须足够才能 PAY"
  • 动作(Action):转换发生时执行的副作用,例如"PAY 成功后扣库存、发短信"

# 2.3 状态机的类型

graph TD
    SM[状态机分类] --> FSM[有限状态机 FSM<br/>状态数量有限可枚举]
    SM --> HSM[层次状态机 HSM<br/>状态可嵌套父子]
    SM --> Concur[并发状态机<br/>正交状态并行]
    
    FSM --> DFA[确定性 DFA<br/>同一状态+事件 → 唯一下一状态]
    FSM --> NFA[非确定性 NFA<br/>同一状态+事件 → 多种可能]
    
    HSM --> UML[UML Statecharts<br/>父状态包含子状态机]
    
    Concur --> Ortho[正交分解<br/>多个独立维度并行]
    
    style FSM fill:#e3f2fd
    style HSM fill:#e8f5e8
    style Concur fill:#fff3e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
类型 特点 典型场景
DFA 确定性 FSM 状态扁平、转换唯一 订单 / 工单 / 审批(90% 业务首选)
NFA 非确定性 FSM 同一输入有多分支 词法分析 / 正则引擎
HSM 层次状态机 父子嵌套、行为继承 游戏 AI / 嵌入式设备
并发状态机 多维度独立演化 UI(登录态 × 网络态 × 权限态)

工程经验:业务系统 90% 用 DFA 就够了,不要一上来就上 HSM 或并发状态机,那是杀鸡用牛刀。

# 2.4 状态机六大好处

这是回答"为什么要用状态机"的核心:

mindmap
  root((状态机六大好处))
    1 收敛状态判断
      30 处 if-else → 1 张转换表
      改规则只改一处
    2 显式业务规则
      转换表 = 活文档
      新人一看就懂
    3 拦截非法操作
      默认拒绝兜底
      事故无处遁形
    4 解耦状态与副作用
      状态变更纯粹
      副作用异步隔离
    5 易于测试与回放
      转换可枚举
      路径全覆盖
    6 业务可视化
      状态图自然导出
      产品/开发同语言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

对照开篇事故,为什么用状态机就不会出 230 万的事故?

事故根因 状态机如何免疫
状态判断散落 30 处 好处 1:所有判断收敛到 fire(event)
新人忘了写状态校验 好处 3:默认拒绝,未声明转换直接抛异常
没人能说清完整规则 好处 2:转换表就是唯一事实来源
短信失败回滚状态 好处 4:副作用走 MQ,状态变更纯粹

# 03.要解决的核心矛盾

# 3.1 状态分散与一致

graph LR
    A[订单状态] --> B1[支付服务<br/>判断 status]
    A --> B2[发货服务<br/>判断 status]
    A --> B3[退款服务<br/>判断 status]
    A --> B4[补偿接口<br/>忘了判断]
    
    B4 --> Bug[💥 出 bug]
    
    Solve[状态机] --> One[所有判断收敛<br/>到一处]
    
    style B4 fill:#ffebee
    style Bug fill:#ffebee
    style One fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 灵活与约束

维度 灵活(无约束) 约束(状态机)
代码自由度 高 受限
业务正确性 靠开发自觉 系统强制
新人上手 容易出错 沿着规则走
重构风险 极高 可控

# 3.3 简单与扩展

最常见误区:业务一上来就上"超复杂状态机框架" → 杀鸡用牛刀。

graph LR
    A[3 个状态<br/>2 条边] --> Simple[简单 if-else 即可]
    B[6 个状态<br/>10 条边] --> Mid[轻量状态机]
    C[20+ 状态<br/>层次结构] --> Heavy[Spring Statemachine / DSL]
    
    style Simple fill:#e3f2fd
    style Mid fill:#e8f5e8
    style Heavy fill:#fff3e0
1
2
3
4
5
6
7
8

# 3.4 状态机的本质

状态机 = 把"什么时候可以做什么"显式建模为代码

它的核心 = 状态 + 事件 + 转换 + 守卫 + 动作 的五元组。

# 04.业界主流方案

# 4.1 主流实现方式

方案 描述 代表
if-else 直接条件判断 最朴素
switch + 转换表 集中转换规则 中小项目
状态模式 每个状态一个类 OOP 经典
状态机框架 声明式 DSL Spring Statemachine / Stateless4j
工作流引擎 可视化建模 Camunda / Activiti
Actor 模型 状态 + 消息 Akka / 游戏引擎

# 4.2 横向对比矩阵

维度 if-else 转换表 状态模式 状态机框架 工作流引擎
学习成本 极低 低 中 中 高
代码量 极少 少 中 配置 配置
可视化 ❌ ❌ ❌ ⚠️ ✅
运行时改规则 ❌ ❌ ❌ ⚠️ ✅
状态规模 < 5 < 20 < 50 任意 任意
典型场景 简单流程 订单 / 工单 复杂业务 中后台 BPM

# 4.3 选型速查

flowchart TD
    Q1{状态数量?} 
    
    Q1 -->|< 5 状态| Simple[if-else / switch<br/>不要过度设计]
    Q1 -->|5-20 状态| Table[转换表 + 状态机封装<br/>主流选择]
    Q1 -->|20+ 状态<br/>有层次嵌套| Frame[Spring Statemachine<br/>专业框架]
    Q1 -->|长流程<br/>需要可视化| BPM[Camunda<br/>工作流引擎]
    
    style Simple fill:#e3f2fd
    style Table fill:#e8f5e8
    style Frame fill:#fff3e0
    style BPM fill:#f3e5f5
1
2
3
4
5
6
7
8
9
10
11
12

# 05.设计核心原则

# 5.1 状态封闭原则

铁律:状态字段只能由状态机修改,业务代码绝不直接 setStatus。

// ❌ 反例 - 业务代码直接改状态
order.setStatus(PAID);
orderRepo.save(order);

// ✅ 正确 - 通过状态机触发事件
stateMachine.fire(order, OrderEvent.PAY);
1
2
3
4
5
6

# 5.2 转换显式原则

所有合法转换必须在一处声明,禁止"代码里偷偷加新转换"。

// 转换规则集中表 - 一目了然
private static final Map<Pair<State, Event>, State> TRANSITIONS = Map.of(
    Pair.of(PENDING, PAY),               PAID,
    Pair.of(PENDING, CANCEL),            CANCELLED,
    Pair.of(PAID, SHIP),                 SHIPPED,
    Pair.of(PAID, CANCEL),               CANCELLED,   // 已支付取消 → 走退款
    Pair.of(SHIPPED, CONFIRM_RECEIPT),   RECEIVED,
    Pair.of(RECEIVED, COMPLETE),         COMPLETED
);
1
2
3
4
5
6
7
8
9

# 5.3 默认拒绝原则

铁律:未在转换表里声明的 (状态, 事件) 组合 = 一律拒绝。

flowchart LR
    Req[fire 事件] --> Lookup[查转换表]
    
    Lookup --> Found{找到?}
    Found -->|有| Allow[执行转换]
    Found -->|无| Deny[拒绝 + 抛异常 + 日志]
    
    style Allow fill:#e8f5e8
    style Deny fill:#ffebee
1
2
3
4
5
6
7
8
9

→ 这就是开篇事故的解药:补偿支付如果走状态机,已发货状态 + PAY 事件根本查不到映射,直接拒绝。

# 5.4 副作用隔离

状态转换 ≠ 副作用,把"改状态"和"做业务(发短信/退款/扣库存)"分离。

graph LR
    Event[事件 PAY] --> SM[状态机]
    SM --> Tx[1 校验转换合法]
    Tx --> Update[2 更新状态字段]
    Update --> Action[3 触发副作用<br/>发通知/发券/...]
    
    Note[副作用失败 ≠ 状态回滚<br/>用消息队列保证最终一致]
    
    style SM fill:#e8f5e8
    style Note fill:#fff3e0
1
2
3
4
5
6
7
8
9
10

# 06.从普通代码到状态机的演变

这一章回答 "业务代码是怎么从最朴素的 if-else 一步步长成状态机的",把演化过程切片展示,让你看到每一步解决了什么问题、引入了什么新约束。

# 6.1 V0 散落式 if-else

业务起步阶段,3 个程序员各写各的接口:

// 支付接口 - 张三写
public boolean payOrder(Order order, double amount) {
    if (order.getStatus() != PENDING) return false;   // 状态判断 1
    payService.charge(amount);
    order.setStatus(PAID);
    return true;
}

// 取消接口 - 李四写
public boolean cancelOrder(Order order) {
    OrderStatus s = order.getStatus();
    if (s == PENDING) {                                // 状态判断 2
        order.setStatus(CANCELLED);
    } else if (s == PAID) {                            // 状态判断 3
        refundService.refund(order);
        order.setStatus(CANCELLED);
    } else if (s == SHIPPED) {                         // 状态判断 4
        logisticsService.intercept(order);
        order.setStatus(CANCELLED);
    } else { return false; }
    return true;
}

// 💥 补偿支付 - 王五新人写(开篇事故根因)
public boolean compensatePay(Order order, double amount) {
    // 忘了状态判断!
    payService.charge(amount);
    order.setStatus(PAID);   // 已发货也能改回 PAID → 二次扣款
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

问题画像:

  • 状态判断分散在 N 个接口,每个新接口都要"记得"写
  • 取消逻辑里 if-else 越来越长,加新状态要改多处
  • 没有任何"全局规则总览",新人无从下手
  • 开篇 230 万事故就发生在这一阶段

# 6.2 V1 集中式状态判断

第一次抽象,把状态判断提取成一个工具方法:

public class OrderStatusValidator {
    public static boolean canPay(OrderStatus s)    { return s == PENDING; }
    public static boolean canShip(OrderStatus s)   { return s == PAID; }
    public static boolean canCancel(OrderStatus s) { return s == PENDING || s == PAID || s == SHIPPED; }
}

// 业务接口都调工具
public boolean payOrder(Order order, double amount) {
    if (!OrderStatusValidator.canPay(order.getStatus())) return false;
    // ...
    order.setStatus(PAID);
}
1
2
3
4
5
6
7
8
9
10
11
12

改进点:

  • ✅ 状态判断从 N 处收敛到 1 个工具类
  • ✅ 改规则只改 Validator

仍未解决的问题:

  • ❌ setStatus(PAID) 依然散落在每个接口,状态变更入口不唯一
  • ❌ "PENDING + PAY → PAID"这条规则没有显式声明,只能从代码推断
  • ❌ 副作用(扣款/退款)和状态变更还混在一起
  • ❌ 新人写接口可能绕过 Validator 直接 setStatus

# 6.3 V2 转换表 + 状态机

真正的状态机,把"事件 → 状态变更"统一收敛:

public class OrderStateMachine {
    
    // 1️⃣ 显式声明所有合法转换
    private static final Map<Key, OrderState> TRANSITIONS = Map.of(
        new Key(PENDING,  PAY),              PAID,
        new Key(PENDING,  CANCEL),           CANCELLED,
        new Key(PAID,     SHIP),             SHIPPED,
        new Key(PAID,     CANCEL),           CANCELLED,
        new Key(SHIPPED,  CONFIRM_RECEIPT),  RECEIVED,
        new Key(RECEIVED, COMPLETE),         COMPLETED
    );
    
    // 2️⃣ 唯一的状态变更入口
    public OrderState fire(Order order, OrderEvent event) {
        OrderState from = order.getStatus();
        OrderState to = TRANSITIONS.get(new Key(from, event));
        if (to == null) {
            throw new IllegalTransitionException("非法转换: " + from + " + " + event);
        }
        order.setStatus(to);
        eventBus.publish(new OrderTransitionEvent(order, from, to, event));  // 副作用异步
        return to;
    }
    record Key(OrderState state, OrderEvent event) {}
}

// 业务接口极简化
public boolean payOrder(Order order)      { return stateMachine.fire(order, PAY) != null; }
public boolean cancelOrder(Order order)   { return stateMachine.fire(order, CANCEL) != null; }
public boolean compensatePay(Order order) { return stateMachine.fire(order, PAY) != null; }
//                                                    ↑ SHIPPED + PAY 直接抛异常 → 事故免疫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

质的飞跃:

  • ✅ 状态变更只有一个入口 fire() → 不可能绕过
  • ✅ 业务规则全部写在转换表 → 一目了然,就是活文档
  • ✅ 默认拒绝 → 新人写新接口想出错都难
  • ✅ 副作用异步发出 → 状态变更原子、可靠
  • ✅ 新增状态/事件 → 加一行映射 + 加一个枚举值,结束

# 6.4 V3 配置化 DSL

演化终点,业务规则频繁变 / 需要运营可配 / 跨多个状态机时:

# YAML 配置文件 - 运营可改、不发版
order-state-machine:
  initial: PENDING
  transitions:
    - from: PENDING,  on: PAY,             to: PAID,      action: chargeAndNotify
    - from: PENDING,  on: CANCEL,          to: CANCELLED, action: releaseStock
    - from: PAID,     on: SHIP,            to: SHIPPED,   action: createLogistics
    - from: PAID,     on: CANCEL,          to: CANCELLED, action: refundAndRelease
    - from: SHIPPED,  on: CONFIRM_RECEIPT, to: RECEIVED,  action: startReviewTimer
    - from: RECEIVED, on: COMPLETE,        to: COMPLETED, action: grantPoints
1
2
3
4
5
6
7
8
9
10

或使用 Spring Statemachine、Camunda BPMN 等专业框架,进一步获得可视化建模 / 运行时改规则 / 完整审计回放能力。

适用阶段:大型 BPM 平台 / SaaS 工作流 / 状态规模 20+ 且经常变。

⚠️ 警告:V3 的成本远高于 V2,99% 业务停在 V2 就够了,盲目上 V3 = 引入下一节的"过度设计反面教材"。

# 6.5 演变路径全景

flowchart LR
    V0[V0 散落 if-else<br/>状态判断遍地] --> V1[V1 集中 Validator<br/>判断收敛]
    V1 --> V2[V2 转换表 + 状态机<br/>★ 主流终点]
    V2 --> V3[V3 配置化 DSL<br/>BPM 平台]
    
    V0 --> P0[痛点:开篇事故<br/>新接口忘判断]
    V1 --> P1[痛点:setStatus 仍散落<br/>规则不显式]
    V2 --> Win2[💎 90% 业务的最佳实践]
    V3 --> P3[警告:过度设计风险]
    
    style V0 fill:#ffebee
    style V1 fill:#fff3e0
    style V2 fill:#e8f5e8
    style V3 fill:#f3e5f5
    style Win2 fill:#c8e6c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
版本 适用阶段 投入成本 收益
V0 仅适合一次性脚本 / Demo 0 短期省事,长期事故源头
V1 业务起步、状态 < 5 ⭐ 比 V0 略好
V2 绝大多数中后台业务 ⭐⭐ 投产比最高
V3 大型 BPM / 规则常变 ⭐⭐⭐⭐ 灵活但复杂

核心建议:直接从 V0 跳到 V2,V1 是过渡形态,没必要专门停留。除非确认有 V3 的强烈业务诉求(运营可配 / 可视化建模),否则不要上 V3。

# 07.方案落地实战

# 7.1 整体架构

graph TB
    subgraph "业务入口"
        API[支付/发货/取消 API]
    end
    
    subgraph "状态机核心"
        Engine[状态机引擎]
        Table[(转换表)]
        Guard[守卫条件]
        Action[动作执行器]
    end
    
    subgraph "存储与外部"
        DB[(订单表 + 状态字段)]
        MQ[消息队列<br/>发副作用事件]
    end
    
    API --> Engine
    Engine --> Table
    Engine --> Guard
    Guard --> Engine
    Engine --> DB
    Engine --> Action
    Action --> MQ
    MQ --> SMS[短信/积分/库存...]
    
    style Engine fill:#e8f5e8
    style Table fill:#fff3e0
    style MQ fill:#e3f2fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 7.2 五元组建模

以订单状态机为例(保留全文最核心的案例):

① 状态枚举

public enum OrderState {
    PENDING,    // 待支付 - 初始
    PAID,       // 已支付
    SHIPPED,    // 已发货
    RECEIVED,   // 已收货
    COMPLETED,  // 已完成 - 终态
    CANCELLED   // 已取消 - 终态
}
1
2
3
4
5
6
7
8

② 事件枚举

public enum OrderEvent {
    PAY, SHIP, CANCEL, CONFIRM_RECEIPT, COMPLETE
}
1
2
3

③ 转换规则表

当前状态 事件 目标状态 副作用
PENDING PAY PAID 扣款 + 通知
PENDING CANCEL CANCELLED 释放库存
PAID SHIP SHIPPED 创建物流单
PAID CANCEL CANCELLED 退款 + 释放库存
SHIPPED CONFIRM_RECEIPT RECEIVED 启动评价定时器
RECEIVED COMPLETE COMPLETED 发积分 + 归档

④ 状态转换图

stateDiagram-v2
    [*] --> PENDING : 创建订单
    PENDING --> PAID : PAY
    PENDING --> CANCELLED : CANCEL
    PAID --> SHIPPED : SHIP
    PAID --> CANCELLED : CANCEL
    SHIPPED --> RECEIVED : CONFIRM_RECEIPT
    RECEIVED --> COMPLETED : COMPLETE
    COMPLETED --> [*]
    CANCELLED --> [*]
1
2
3
4
5
6
7
8
9
10

# 7.3 状态机重构核心

核心思路:所有状态变更必走 fire(event)。

public class OrderStateMachine {
    
    // 转换规则表 - 唯一的事实来源
    private static final Map<Key, OrderState> TRANSITIONS = Map.of(
        new Key(PENDING,  PAY),              PAID,
        new Key(PENDING,  CANCEL),           CANCELLED,
        new Key(PAID,     SHIP),             SHIPPED,
        new Key(PAID,     CANCEL),           CANCELLED,
        new Key(SHIPPED,  CONFIRM_RECEIPT),  RECEIVED,
        new Key(RECEIVED, COMPLETE),         COMPLETED
    );
    
    /**
     * 唯一的状态变更入口
     */
    public OrderState fire(Order order, OrderEvent event) {
        OrderState from = order.getStatus();
        OrderState to = TRANSITIONS.get(new Key(from, event));
        
        // 默认拒绝
        if (to == null) {
            throw new IllegalTransitionException(
                String.format("非法转换: %s + %s", from, event));
        }
        
        // 执行转换 + 副作用(解耦发到 MQ)
        order.setStatus(to);
        orderRepo.save(order);
        eventBus.publish(new OrderTransitionEvent(order, from, to, event));
        
        log.info("Order[{}] {} --{}--> {}", order.getId(), from, event, to);
        return to;
    }
    
    record Key(OrderState state, OrderEvent event) {}
}

// 业务代码极简
public class OrderService {
    public void payOrder(Order order)    { stateMachine.fire(order, PAY); }
    public void shipOrder(Order order)   { stateMachine.fire(order, SHIP); }
    public void cancelOrder(Order order) { stateMachine.fire(order, CANCEL); }
    
    // 💡 补偿支付?同样走状态机 → 已发货状态根本走不通
    public void compensatePay(Order order) { 
        stateMachine.fire(order, PAY);  // SHIPPED + PAY 直接抛异常
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

重构后开篇事故自动免疫:补偿支付在已发货状态调 fire(order, PAY) → 转换表查不到 → 异常拒绝。

# 7.4 转换流程

sequenceDiagram
    participant Biz as 业务代码
    participant SM as 状态机
    participant Table as 转换表
    participant DB as 数据库
    participant MQ as 消息队列
    
    Biz->>SM: fire(order, PAY)
    SM->>Table: lookup(PENDING, PAY)
    
    alt 转换合法
        Table-->>SM: PAID
        SM->>DB: 更新状态字段
        SM->>MQ: 发布 OrderPaidEvent
        SM-->>Biz: 成功
        MQ-->>SM: 消费者处理副作用<br/>扣库存/发短信...
    else 转换非法
        Table-->>SM: null
        SM-->>Biz: 抛 IllegalTransitionException
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 08.关键问题解决

# 8.1 状态爆炸

典型场景:登录(4) × 权限(4) × 网络(4) = 64 状态,直接爆炸。

graph LR
    A[问题: 多维状态<br/>组合爆炸] --> S[解法]
    
    S --> S1[1 拆分独立状态机<br/>登录/权限/网络分开]
    S --> S2[2 层次状态机<br/>父状态 + 子状态]
    S --> S3[3 并发状态机<br/>正交状态]
    
    style A fill:#ffebee
    style S fill:#e8f5e8
1
2
3
4
5
6
7
8
9

实战经验:单个状态机超过 10 个状态就要考虑拆分。

# 8.2 状态持久化

关键问题:进程重启 / 多机部署 → 状态在哪?

方案 1: 状态字段持久化(最常用)
    在业务表加 status 字段
    优点: 简单、和业务一体
    缺点: 历史轨迹查不到

方案 2: 状态 + 事件流持久化
    业务表 status + 独立事件表
    优点: 可审计、可回放
    缺点: 多写一张表

方案 3: 事件溯源(Event Sourcing)
    只存事件、状态由事件回放得到
    优点: 完整历史
    缺点: 复杂、查询麻烦
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.3 并发与幂等

真实坑:用户双击"支付按钮" → 两个请求同时进 fire(PAY) → 都通过校验 → 状态被改两次 + 扣款两次。

// ✅ 用 CAS 乐观锁保证只有一次成功
public OrderState fire(Order order, OrderEvent event) {
    OrderState from = order.getStatus();
    OrderState to = TRANSITIONS.get(new Key(from, event));
    if (to == null) throw new IllegalTransitionException(...);
    
    // CAS: 只有 status = from 时才能改成 to
    int updated = orderRepo.updateStatusCAS(order.getId(), from, to);
    if (updated == 0) {
        throw new ConcurrentTransitionException("状态已被其他请求修改");
    }
    
    eventBus.publish(...);
    return to;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- CAS SQL
UPDATE orders SET status = ?, version = version + 1 
WHERE id = ? AND status = ?
1
2
3

# 09.常见陷阱与反例

# 9.1 状态散布反例

反例:30 个文件里都有 if (order.status == ...) → 加新状态要改 30 处 → 必漏。

教训:

  • 状态判断收敛到状态机
  • 业务代码只调 fire(event),不直接读 status 做分支

# 9.2 副作用混入反例

反例:在状态机内部直接发短信 / 调支付 → 短信失败导致状态回滚 → 数据错乱。

教训:

  • 状态机只负责改状态字段
  • 副作用异步发出(MQ / 事件总线)
  • 副作用失败不影响状态变更

# 9.3 过度设计反面教材

真实案例:某创业公司做内部审批系统,只有 3 个状态、2 条转换线:

DRAFT --提交--> SUBMITTED --审批通过--> APPROVED
                          --审批驳回--> DRAFT
1
2

技术负责人为了"显得专业",决定上 Spring Statemachine + XML 配置:

<!-- 50+ 行 XML 配置 -->
<state-machine id="approvalMachine" initial-state="DRAFT">
    <state id="DRAFT"/>
    <state id="SUBMITTED"/>
    <state id="APPROVED" final="true"/>
    <transitions>
        <transition source="DRAFT"     target="SUBMITTED" event="SUBMIT"/>
        <transition source="SUBMITTED" target="APPROVED"  event="APPROVE"/>
        <transition source="SUBMITTED" target="DRAFT"     event="REJECT"/>
    </transitions>
    <!-- 还有一堆 listener / action / guard 配置 -->
</state-machine>
1
2
3
4
5
6
7
8
9
10
11
12

结果:

  • ❌ 引入框架后项目代码量从 50 行膨胀到 500 行
  • ❌ 团队没人懂 Spring Statemachine 调试 → 出 bug 排查 3 天
  • ❌ 新人入职第一周 = 学框架,学不会还不敢动审批模块
  • ❌ 加一个新状态 → 要改 XML + 监听器 + 守卫类 + 单测,比 if-else 慢 5 倍
  • ❌ 半年后接手的团队怒了,直接删掉框架,回到 30 行 switch-case
// 后来重写的版本 - 30 行搞定
public class ApprovalMachine {
    private static final Map<Key, Status> T = Map.of(
        new Key(DRAFT,     SUBMIT),  SUBMITTED,
        new Key(SUBMITTED, APPROVE), APPROVED,
        new Key(SUBMITTED, REJECT),  DRAFT
    );
    public Status fire(Approval a, Event e) {
        Status to = T.get(new Key(a.getStatus(), e));
        if (to == null) throw new IllegalStateException();
        a.setStatus(to);
        return to;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

血泪教训:

误区 真相
"框架显得专业" 专业 ≠ 复杂,简单解决问题才专业
"万一以后要扩展呢" YAGNI 原则,等真要扩展再演进,提前设计 99% 是过度设计
"状态机 = 一定要用框架" V2 的 30 行手写代码已经是状态机了
"可视化建模很酷" 3 个状态根本不需要可视化

⚠️ 过度使用状态机的危害:增加学习成本、增加调试难度、增加维护负担、降低开发速度,性价比为负。

# 9.4 何时不要用状态机

反向决策,下列情况坚决不要上状态机:

flowchart TD
    Q[要不要用状态机?] --> C1{状态数 < 4?}
    C1 -->|是| No1[❌ 不用<br/>if-else 更短更清楚]
    
    Q --> C2{是简单线性流程?<br/>无分支无回退}
    C2 -->|是| No2[❌ 不用<br/>顺序执行即可]
    
    Q --> C3{状态边界模糊?<br/>说不清有几个状态}
    C3 -->|是| No3[❌ 不用<br/>先把业务搞清楚]
    
    Q --> C4{需要极致性能?<br/>每秒百万次状态变更}
    C4 -->|是| No4[⚠️ 谨慎<br/>转换表查询有开销]
    
    Q --> C5{规则一天三变?<br/>且没人能定下来}
    C5 -->|是| No5[⚠️ 慎用<br/>先稳定业务再上]
    
    Q --> Yes[✅ 用状态机<br/>状态明确 / 多分支 / 需要约束]
    
    style No1 fill:#ffebee
    style No2 fill:#ffebee
    style No3 fill:#ffebee
    style No4 fill:#fff3e0
    style No5 fill:#fff3e0
    style Yes fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
不用状态机的场景 理由 替代方案
状态 < 4 个 引入状态机的脚手架成本 > 收益 if-else / switch
简单线性流程 无回退、无分支,顺序执行即可 直接顺序代码
状态边界模糊 状态机要求"互斥且穷尽",模糊状态会越绕越乱 先做业务建模
极致性能场景 Map 查询 + 异常抛出有开销 直接判断或位运算
业务规则未稳定 频繁改状态机比改 if-else 还痛苦 先 if-else 跑通
mindmap
  root((三大反例 + 一个反方向))
    9.1 状态散布
      30 处 if-else
      改 1 个状态炸全场
      → 收敛到状态机
    9.2 副作用混入
      短信失败 → 状态回滚
      数据错乱
      → MQ 异步隔离
    9.3 过度设计
      3 状态用大框架
      杀鸡用牛刀
      → 按规模选方案
    9.4 不该用却用了
      状态< 4 / 线性流程
      → 直接 if-else
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10.总结与决策

# 10.1 上线检查表

状态机方案上线前对照:

  • [ ] 状态枚举完整(含初始态、终态)
  • [ ] 事件枚举完整
  • [ ] 转换规则表集中声明(一处事实来源)
  • [ ] 唯一 fire(event) 入口
  • [ ] 业务代码不直接 setStatus
  • [ ] 默认拒绝(未声明的 (状态, 事件) 抛异常)
  • [ ] CAS 乐观锁防并发
  • [ ] 副作用异步化(MQ / 事件总线)
  • [ ] 状态变更日志(可审计)
  • [ ] 单元测试覆盖所有合法 + 非法转换
  • [ ] 监控(非法转换告警、状态分布)
  • [ ] 文档(状态图同步更新)

# 10.2 选型决策树

flowchart TD
    Start([业务有状态流转?]) --> First{状态数量?}
    
    First -->|< 4 / 线性流程| Skip[❌ 不用状态机<br/>if-else 即可]
    First -->|4-20| Table[✅ 转换表 + 状态机封装<br/>90% 业务首选 = V2]
    First -->|20+ / 层次嵌套| Frame[Spring Statemachine<br/>专业框架 = V3]
    First -->|长流程 / 可视化 / 运营可配| BPM[Camunda / Activiti<br/>工作流引擎 = V3]
    
    Q2([需要并发安全?]) -->|是| CAS[CAS 乐观锁]
    Q3([需要审计追溯?]) -->|是| ES[事件溯源 / 状态日志]
    Q4([业务规则常变?]) -->|是| Cfg[配置化 / DSL]
    
    style Skip fill:#ffebee
    style Table fill:#c8e6c9
    style Frame fill:#fff3e0
    style BPM fill:#f3e5f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后一句话:状态机不是"高大上的架构",而是 "把业务规则变成代码强制约束" 的最小武器。开篇 1.7 万笔重复扣款的根因,不是技术不行,是状态规则没收敛、新接口忘校验,而状态机能让"忘校验"从根本上不可能发生。

好的状态机 = 状态封闭、转换显式、默认拒绝、副作用隔离。

但更重要的是,不要为了用状态机而用状态机:3 个状态用 30 行手写转换表就够了,引入大框架反而是事故的另一个源头。

上次更新: 2026/06/07, 10:26:12
通用轮训方案设计
通用搜索方案设计

← 通用轮训方案设计 通用搜索方案设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式