1. 状态迁移基础:状态机、三要素与状态图

状态迁移,说白了就是描述一个系统「在什么条件下,从什么状态,变成什么状态」。我做了十几年系统设计,见过太多因为状态没理清而翻车的项目。有一次线上服务莫名其妙地挂掉,查了三天才发现是状态机漏了一个边界事件。嗯,从那以后,我写任何带状态的模块,第一件事就是画状态图。

1.1 什么是状态机

状态机(State Machine)是一种计算模型。它告诉我们:系统在任意时刻只能处于有限个状态中的一个。当某个事件发生时,系统会从当前状态迁移到另一个状态,并执行相应的动作。

你想想看,电梯就是一个典型的状态机:

  • 它要么停在某层(静止),要么在移动(上行或下行)
  • 按了楼层按钮(事件),它决定是开门、关门还是继续移动
  • 不可能同时「开门中」又「移动中」——这就是状态互斥

我个人习惯把状态机分成两类:

类型 特点 典型场景
Mealy 型 输出依赖当前状态 + 输入事件 协议解析、按键消抖
Moore 型 输出只依赖当前状态 电梯控制、订单流程
我的经验:实际项目中 Mealy 型更灵活,但 Moore 型更容易调试。如果你团队新人多,优先选 Moore 型,状态和动作一一对应,出问题一眼就能看出来。

1.2 核心三要素:状态、事件、动作

状态机的核心就三个东西。我每次给团队培训都这么说:状态是「在哪」,事件是「发生了什么」,动作是「做什么」。

1.2.1 状态(State)

状态是系统在某个时刻的稳定快照。它必须满足:

  • 可枚举:所有可能的状态列得出来,不能有「其他」这种模糊项
  • 互斥:同一时刻只能在一个状态里
  • 有初始态和终态:从哪里开始,到哪里结束

我在项目中遇到过一个问题:订单状态表里加了「已取消」和「已退款」,但没区分「退款中」和「退款失败」。结果财务对账时差了十几万。说白了,状态粒度不够细,就会埋雷。

1.2.2 事件(Event)

事件是触发状态迁移的外部或内部信号。事件可以带参数,比如「支付成功(金额=100, 渠道=)」。

事件分类:

  • 外部事件:用户点击、网络请求、硬件中断
  • 内部事件:定时器超时、内部状态变更通知
  • 系统事件:启动、关机、异常
避坑指南:我曾经在一个嵌入式项目里,把「按键按下」和「按键释放」当成两个独立事件处理,结果漏掉了「长按」场景。后来改成事件携带持续时间参数,才彻底解决。记住:事件要完整描述「发生了什么」,而不是「你觉得发生了什么」。

1.2.3 动作(Action)

动作是迁移发生时执行的操作。动作可以发生在:

  • 进入状态时(entry action)
  • 离开状态时(exit action)
  • 迁移过程中(transition action)

举个例子,一个门禁系统:

  • 进入「已锁定」状态 → 关闭电磁锁、点亮红灯
  • 离开「已锁定」状态 → 解除电磁锁供电
  • 收到「刷卡成功」事件 → 记录日志、播放提示音

1.3 状态迁移图的基本画法

状态迁移图(State Transition Diagram)是状态机的可视化表达。我建议每个模块设计阶段都画一张,哪怕只是草稿。它能帮你发现「不可能发生的迁移」和「遗漏的事件」。

基本元素就四个:

  • 圆角矩形:表示状态
  • 实心圆点:表示初始状态
  • 带圆圈的圆点:表示终止状态
  • 箭头 + 标签:表示迁移,标签格式为「事件 / 动作」

下面是一个简单的订单状态机示例:

订单状态迁移图示例 S 待支付 已支付 已发货 已完成 已取消 支付成功 超时取消 发货 申请退款 确认收货 说明: 每个迁移箭头上的标签格式为「事件 / 动作」,本例中只标注了事件名。实际项目中建议同时标注动作。

画图要点(我踩过的坑):

  • 每个状态至少有一条入边和一条出边(初始和终止状态除外)
  • 不要有「悬空」的事件——每个事件必须对应一个明确的迁移
  • 用不同颜色区分正常流程和异常流程,比如绿色表示正常,红色表示异常
  • 复杂状态机建议分层:先画顶层,再逐层展开子状态

1.4 状态迁移表:另一种表达方式

除了状态图,状态迁移表也是一种常用的表达方式。我个人习惯先画图,再转成表,因为表更容易做自动化测试。

当前状态 事件 下一状态 动作
待支付 支付成功 已支付 扣减库存、发送通知
待支付 超时取消 已取消 释放预占库存
已支付 发货 已发货 生成物流单、通知用户
已支付 申请退款 已取消 发起退款流程
已发货 确认收货 已完成 结算、评价邀请
小技巧:写状态迁移表时,把「不可能发生的迁移」也列出来,比如「已取消 → 已支付」。虽然它不会发生,但明确写出来能防止代码里出现意外的逻辑漏洞。我曾经就因为没写这个,导致一个定时任务错误地把已取消的订单重新激活了。

1.5 状态机设计的基本原则

最后,分享几条我这些年总结出来的原则:

  1. 状态要少,事件要全:状态多了难以维护,事件漏了容易出bug
  2. 每个状态都要有超时处理:系统不可能永远等下去
  3. 状态迁移必须是确定性的:同样的状态 + 同样的事件,结果必须一样
  4. 记录状态变更历史:出了问题才能回溯
  5. 状态机要可测试:每个迁移路径都要能覆盖到
避坑指南:我曾经接手过一个遗留系统,状态机没有持久化,服务重启后所有状态都丢了。结果用户订单卡在「支付中」状态,既不能继续支付,也不能取消。后来我们花了整整一周才把数据修回来。记住:状态机一定要持久化,而且每次状态变更都要写日志。

好了,状态机的基础就这些。说白了,它就是一套「在什么情况下做什么事」的规则。把状态、事件、动作理清楚,你的系统就成功了一半。