1. 状态迁移基础:状态机、三要素与状态图
状态迁移,说白了就是描述一个系统「在什么条件下,从什么状态,变成什么状态」。我做了十几年系统设计,见过太多因为状态没理清而翻车的项目。有一次线上服务莫名其妙地挂掉,查了三天才发现是状态机漏了一个边界事件。嗯,从那以后,我写任何带状态的模块,第一件事就是画状态图。
1.1 什么是状态机
状态机(State Machine)是一种计算模型。它告诉我们:系统在任意时刻只能处于有限个状态中的一个。当某个事件发生时,系统会从当前状态迁移到另一个状态,并执行相应的动作。
你想想看,电梯就是一个典型的状态机:
- 它要么停在某层(静止),要么在移动(上行或下行)
- 按了楼层按钮(事件),它决定是开门、关门还是继续移动
- 不可能同时「开门中」又「移动中」——这就是状态互斥
我个人习惯把状态机分成两类:
| 类型 | 特点 | 典型场景 |
|---|---|---|
| Mealy 型 | 输出依赖当前状态 + 输入事件 | 协议解析、按键消抖 |
| 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)是状态机的可视化表达。我建议每个模块设计阶段都画一张,哪怕只是草稿。它能帮你发现「不可能发生的迁移」和「遗漏的事件」。
基本元素就四个:
- 圆角矩形:表示状态
- 实心圆点:表示初始状态
- 带圆圈的圆点:表示终止状态
- 箭头 + 标签:表示迁移,标签格式为「事件 / 动作」
下面是一个简单的订单状态机示例:
画图要点(我踩过的坑):
- 每个状态至少有一条入边和一条出边(初始和终止状态除外)
- 不要有「悬空」的事件——每个事件必须对应一个明确的迁移
- 用不同颜色区分正常流程和异常流程,比如绿色表示正常,红色表示异常
- 复杂状态机建议分层:先画顶层,再逐层展开子状态
1.4 状态迁移表:另一种表达方式
除了状态图,状态迁移表也是一种常用的表达方式。我个人习惯先画图,再转成表,因为表更容易做自动化测试。
| 当前状态 | 事件 | 下一状态 | 动作 |
|---|---|---|---|
| 待支付 | 支付成功 | 已支付 | 扣减库存、发送通知 |
| 待支付 | 超时取消 | 已取消 | 释放预占库存 |
| 已支付 | 发货 | 已发货 | 生成物流单、通知用户 |
| 已支付 | 申请退款 | 已取消 | 发起退款流程 |
| 已发货 | 确认收货 | 已完成 | 结算、评价邀请 |
1.5 状态机设计的基本原则
最后,分享几条我这些年总结出来的原则:
- 状态要少,事件要全:状态多了难以维护,事件漏了容易出bug
- 每个状态都要有超时处理:系统不可能永远等下去
- 状态迁移必须是确定性的:同样的状态 + 同样的事件,结果必须一样
- 记录状态变更历史:出了问题才能回溯
- 状态机要可测试:每个迁移路径都要能覆盖到
好了,状态机的基础就这些。说白了,它就是一套「在什么情况下做什么事」的规则。把状态、事件、动作理清楚,你的系统就成功了一半。