vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
---
|
||||
tags: [pattern/fsm, workflow-engine, order-lifecycle, state-machine-pattern]
|
||||
create time: 2026-08-08 15:00
|
||||
update time: 2026-08-08 15:00
|
||||
---
|
||||
|
||||
# 有限状态机在业务中的应用
|
||||
|
||||
## 概述
|
||||
|
||||
FSM 在工程中最大的价值是将"散落的状态判断"收拢到"显式的状态定义"。本文讨论三种典型的业务场景:审批流、订单生命周期和资源交付,每种场景展示其状态图和关键注意事项。
|
||||
|
||||
## 审批流
|
||||
|
||||
### 典型流程
|
||||
|
||||
任务从提交到最终结果需经过多级审批,每个审批人可以批准或驳回。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Draft
|
||||
Draft --> Submitted: 提交申请
|
||||
Submitted --> ManagerReview: 主管审核
|
||||
ManagerReview --> Approved: 批准
|
||||
ManagerReview --> Rejected: 驳回
|
||||
ManagerReview --> ManagerRevise: 修改后重提
|
||||
Approved --> FinanceReview: 进入财务审核
|
||||
FinanceReview --> FinalApproved: 财务批准
|
||||
FinanceReview --> Rejected: 财务驳回
|
||||
Rejected --> [*]
|
||||
FinalApproved --> [*]
|
||||
ManagerRevise --> Draft: 退回草稿
|
||||
```
|
||||
|
||||
### 关键注意事项
|
||||
|
||||
| 注意点 | 说明 |
|
||||
|--------|------|
|
||||
| 权限校验 | 每个审批节点需要确认审批人是否有该角色的审批权限 |
|
||||
| 驳回的语义 | "驳回"可以是终止(回终态)也可以是退回上一步(回到前置状态),需要在模型中明确区分 |
|
||||
| 超期自动处理 | 审批超过 N 天未操作应触发超时事件,默认通过或升级给上级 |
|
||||
| 审计追踪 | 每次状态转换记录 `operator`、`timestamp`、`comment`,不可删除 |
|
||||
|
||||
> [!TIP]
|
||||
> 面试常考点:如果审批链动态变化(比如 A 请假了由 B 代审),这不再是静态 FSM,而是需要引入**策略模式 + 规则引擎**。硬编码审批人在生产环境中是不可接受的。
|
||||
|
||||
## 订单生命周期
|
||||
|
||||
### 状态图
|
||||
|
||||
订单是最经典的 FSM 应用场景。合法转移必须严格受控,防止"跳状态"。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PendingPayment
|
||||
PendingPayment --> Paid: 用户支付
|
||||
PendingPayment --> Cancelled: 超时取消
|
||||
Paid --> Shipped: 仓库发货
|
||||
Paid --> Refunding: 申请退款
|
||||
Shipped --> Completed: 用户确认收货
|
||||
Shipped --> Refunding: 申请退款
|
||||
Completed --> [*]
|
||||
Refunding --> Refunded: 退款完成
|
||||
Refunding --> Paid: 退款拒绝
|
||||
Cancelled --> [*]
|
||||
Refunded --> [*]
|
||||
```
|
||||
|
||||
### 非法转移示例
|
||||
|
||||
```
|
||||
Paid -> Cancelled ❌ 已支付的订单不能直接取消
|
||||
Completed -> Shipped ❌ 已完成不能倒退回已发货
|
||||
PendingPayment -> Shipped ❌ 未付款不能直接发货
|
||||
```
|
||||
|
||||
### 代码示例
|
||||
|
||||
```go
|
||||
func (m *OrderManager) Transition(ctx context.Context, orderID string, event string) error {
|
||||
// 1. 获取当前状态
|
||||
order, _ := m.repo.FindByID(ctx, orderID)
|
||||
|
||||
// 2. 查表确认转换是否合法
|
||||
handler, ok := m.transitions[order.Status][event]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid transition: %s -> [%s]", order.Status, event)
|
||||
}
|
||||
|
||||
// 3. 执行转换
|
||||
if err := handler(ctx, order); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.repo.Save(ctx, order)
|
||||
}
|
||||
```
|
||||
|
||||
## 资源交付八阶段(七牛云存储场景)
|
||||
|
||||
这是一个更复杂的 FSM 应用。假设我们正在实现一个对象存储服务,资源从创建到销毁经历以下八个阶段。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> BucketCreated
|
||||
BucketCreated --> FileUploaded: 上传文件
|
||||
FileUploaded --> CDNPrefetch: CDN预热
|
||||
CDNPrefetch --> AccessAuthorized: 设置访问策略
|
||||
AccessAuthorized --> TrafficTracked: 采集流量统计
|
||||
TrafficTracked --> LifecycleConfigured: 配置生命周期
|
||||
LifecycleConfigured --> CleanupPending: 满足清理条件
|
||||
CleanupPending --> [*]: 物理删除
|
||||
```
|
||||
|
||||
### 各阶段职责
|
||||
|
||||
| 阶段 | 核心操作 | 失败回退策略 |
|
||||
|------|---------|-------------|
|
||||
| BucketCreated | 创建存储空间,分配地域和冗余策略 | 幂等重试,已有 bucket 视为成功 |
|
||||
| FileUploaded | PUT 对象请求,校验 ETag 一致性 | 断点续传 + 分片合并 |
|
||||
| CDNPrefetch | 向 CDN 节点下发缓存指令 | 异步队列,不阻塞主链路 |
|
||||
| AccessAuthorized | 签发临时签名 URL 或配置 ACL | 与文件上传绑定,原子操作 |
|
||||
| TrafficTracked | 持续指标上报至时序数据库 | 本地 buffer,批量异步发送 |
|
||||
| LifecycleConfigured | 设置过期规则(如 90 天自动删除) | API 级别幂等更新 |
|
||||
| CleanupPending | 定时任务扫描到期对象,标记为删除中 | 二次确认后执行,可撤销 |
|
||||
| 物理删除 | 实际调用存储 API 删除对象 | 事务性删除,失败告警 |
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
> [!WARNING]
|
||||
> 资源删除必须是**两阶段**的:先标记 `CleanupPending`,再执行 `物理删除`。这避免误删且支持回滚。跳过这个阶段是数据丢失的头号原因。
|
||||
|
||||
> [!NOTE]
|
||||
> 为什么"流量统计"被建模为状态而不是事件?因为流量统计是一个**持续的监控行为**,不是一个瞬时动作。在实际工程中,它可能通过侧车(sidecar)或事件总线持续运行,不受单用户的控制流影响。FSM 在这里只记录"是否已进入此阶段"。
|
||||
|
||||
## 对比总结
|
||||
|
||||
| 维度 | 审批流 | 订单生命周期 | 资源交付 |
|
||||
|------|--------|-------------|---------|
|
||||
| 状态数量 | 5-8 | 6-8 | 8 |
|
||||
| 循环转移 | 有(退回修改) | 部分(退款→重新支付) | 无(线性流程) |
|
||||
| 并发安全要求 | 中等(单人审批) | **极高**(多人同时操作) | 高(异步任务冲突) |
|
||||
| 持久化策略 | 每步写 DB | 每步写 DB + 事件日志 | 最终一致 + 补偿机制 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[有限状态机核心概念]]
|
||||
@@ -0,0 +1,179 @@
|
||||
---
|
||||
tags: [pattern/fsm, finite-state-machine, state-transition, dfa-nfa, fsm-implementation]
|
||||
create time: 2026-08-08 14:30
|
||||
update time: 2026-08-08 14:30
|
||||
---
|
||||
|
||||
# 有限状态机核心概念
|
||||
|
||||
## 概述
|
||||
|
||||
有限状态机(Finite State Machine,简称 FSM)是一种数学计算模型,也是软件工程中最实用的抽象之一。它将任何有明确"状态"和"动作"的系统建模为一组离散状态的集合,配合事件驱动的转换规则,让复杂业务流程变得可预测、可测试、可追溯。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 四大核心概念
|
||||
|
||||
| 概念 | 定义 | 示例 |
|
||||
|------|------|------|
|
||||
| 状态 (State) | 系统在某一时刻所处的条件或模式 | `PENDING`、`APPROVED`、`REJECTED` |
|
||||
| 事件 (Event) | 触发状态转换的外部或内部信号 | `submit`、`approve`、`reject` |
|
||||
| 转换 (Transition) | 从一个状态到另一个状态的映射关系 | `PENDING + approve -> APPROVED` |
|
||||
| 动作 (Action) | 状态转换时或进入状态时执行的副作用 | 发送邮件通知、写审计日志 |
|
||||
|
||||
一个完整的 FSM 可以用五元组表示:`(States, Events, Transitions, Actions, StartState)`
|
||||
|
||||
### DFA vs NFA
|
||||
|
||||
确定性有限自动机(DFA)和非确定性有限自动机(NFA)是两种理论模型,区别在于同一个状态下对同一事件的响应是否唯一。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph DFA["DFA - 确定性有限自动机"]
|
||||
A["状态A"] -->|"事件X"| B["状态B"]
|
||||
A -->|"事件Y"| C["状态C"]
|
||||
B -->|"事件X"| D["状态D"]
|
||||
B -->|"事件Y"| C
|
||||
end
|
||||
|
||||
subgraph NFA["NFA - 非确定性有限自动机"]
|
||||
E["状态E"] -->|"事件X"| F["状态F"]
|
||||
E -->|"事件X"| G["状态G"]
|
||||
E -->|"事件Y"| H["状态H"]
|
||||
end
|
||||
```
|
||||
|
||||
关键区别:
|
||||
|
||||
- **DFA**:每个状态对每个事件最多只有一个后继状态。行为可预测,适合工程实现。
|
||||
- **NFA**:同一个状态对同一事件可能转移到多个后继状态,甚至可以无转移地"猜"一条路径。等价于 DFA 但更紧凑,适合正则表达式引擎底层。
|
||||
|
||||
> [!NOTE]
|
||||
> 工程实践中的 FSM 几乎总是 DFA。Go 里的 switch-case、map-of-function-map、接口模式本质上都是 DFA 的编程表达。
|
||||
|
||||
### 状态转移方程
|
||||
|
||||
FSM 的核心是转移函数 δ(delta):
|
||||
|
||||
```
|
||||
δ(current_state, event) → next_state
|
||||
```
|
||||
|
||||
当转移存在 Action 时扩展为:
|
||||
|
||||
```
|
||||
δ(current_state, event) → (next_state, action)
|
||||
```
|
||||
|
||||
约束条件:
|
||||
- 转移函数必须是**部分函数** — 并非所有事件在所有状态都有效。例如订单已经"已退款"后不能再"支付"。
|
||||
- 非法转移应返回错误或被拒绝,而非静默忽略。
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 方式一:switch-case 枚举
|
||||
|
||||
最直观的方式,适合状态数少于 10 的场景。
|
||||
|
||||
```go
|
||||
type OrderStatus int
|
||||
|
||||
const (
|
||||
Pending OrderStatus = iota
|
||||
Paid
|
||||
Shipped
|
||||
Completed
|
||||
Refunding
|
||||
Refunded
|
||||
)
|
||||
|
||||
func (o *Order) Handle(event string) error {
|
||||
switch o.Status {
|
||||
case Pending:
|
||||
if event == "pay" {
|
||||
o.Status = Paid
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot %s from %s", event, o.Status)
|
||||
case Paid:
|
||||
if event == "ship" {
|
||||
o.Status = Shipped
|
||||
return nil
|
||||
}
|
||||
// ...
|
||||
}
|
||||
return fmt.Errorf("invalid transition")
|
||||
}
|
||||
```
|
||||
|
||||
### 方式二:map-of-function-map
|
||||
|
||||
将状态转成数据驱动,新增状态只需注册新规则,无需改 switch。
|
||||
|
||||
```go
|
||||
type FSM struct {
|
||||
transitions map[State]map[string]func() error
|
||||
State State
|
||||
}
|
||||
|
||||
func (f *FSM) Register(state State, event string, fn func() error) {
|
||||
if f.transitions[state] == nil {
|
||||
f.transitions[state] = make(map[string]func() error)
|
||||
}
|
||||
f.transitions[state][event] = fn
|
||||
}
|
||||
|
||||
func (f *FSM) Fire(event string) error {
|
||||
handlers, ok := f.transitions[f.State][event]
|
||||
if !ok {
|
||||
return fmt.Errorf("no handler for %q in state %v", event, f.State)
|
||||
}
|
||||
return handlers()
|
||||
}
|
||||
```
|
||||
|
||||
### 方式三:State 接口 + Context 模式
|
||||
|
||||
面向对象的 FSM 设计,每个状态独立封装行为。
|
||||
|
||||
```go
|
||||
type State interface {
|
||||
Enter(ctx *Context) error
|
||||
Handle(event string) (State, error)
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
state State
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func (c *Context) Fire(event string) error {
|
||||
next, err := c.state.Handle(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := next.Enter(c); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = next
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 实践场景
|
||||
|
||||
| 选型策略 | 适用场景 | 优点 | 缺点 |
|
||||
|---------|---------|------|------|
|
||||
| switch-case | 状态少(≤10)、逻辑简单 | 直观、调试容易 | 违反开闭原则 |
|
||||
| map-of-function | 状态中等(10~50)、需要灵活扩展 | 注册式扩展、易测试 | 闭包上下文传递稍繁琐 |
|
||||
| State 接口 | 状态多(>50)、每个状态行为复杂 | 单一职责、面向对象 | 有一定架构开销 |
|
||||
|
||||
> [!TIP]
|
||||
> 面试常考点:为什么不建议用数字枚举 + 硬编码数组来做状态校验?因为数字枚举不具备语义表达能力,编译期无法捕获非法转移。
|
||||
|
||||
> [!WARNING]
|
||||
> 常见误区:把 FSM 当成普通的 if-else。FSM 的核心价值是**把所有合法转换集中在一处定义**,而不是散落在业务逻辑各处。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[有限状态机在业务中的应用]]
|
||||
Reference in New Issue
Block a user