状态机基础:状态的定义、事件的触发、状态转移表、状态转移图
好,咱们正式开始聊状态机。说实话,很多做交易系统的人一听到「状态机」三个字就觉得头大,觉得这是搞底层协议的人才会碰的东西。但我个人的经验是——不理解状态机,你根本写不出靠谱的交易所协议解析代码。
为什么?因为交易所的协议本质上就是一个巨大的状态机。你发一个订单,它从「已提交」变成「待成交」,再变成「部分成交」,最后变成「全部成交」或「已撤销」。每一步都有严格的规则,乱不得。
那咱们今天就把状态机的基础掰开揉碎,讲清楚四个核心概念:状态的定义、事件的触发、状态转移表、状态转移图。
1. 状态的定义——你到底在描述什么?
状态,说白了就是「系统在某个时刻的稳定快照」。你想想看,一个订单在交易所里,它要么在排队,要么已经成交了,要么被撤了。不可能同时处于两个状态,对吧?
我在项目中遇到过最典型的坑:有人把「已报待撤」和「已撤单」混为一谈。这两个状态在协议层面是完全不同的——前者意味着撤单指令已经发出但还没确认,后者是确认完成。如果你把它们当成同一个状态处理,后续的成交回报就会乱套。
定义状态时,我建议遵循三个原则:
- 互斥性:同一时刻只能处于一个状态
- 完备性:所有可能的情况都要覆盖到
- 可观测性:状态必须能通过某种方式被识别
拿一个简单的订单生命周期举例,常见的状态定义如下:
| 状态名称 | 含义 | 典型场景 |
|---|---|---|
| INIT | 初始状态 | 订单刚创建,还没发出去 |
| PENDING | 已提交,等待确认 | 指令已发出,等交易所回执 |
| ACTIVE | 订单在委托簿中 | 已确认,正在排队 |
| PARTIAL_FILLED | 部分成交 | 成交了一部分,剩余还在排队 |
| FILLED | 全部成交 | 订单完全成交 |
| CANCELLED | 已撤销 | 主动撤单或被交易所拒绝 |
2. 事件的触发——谁在推动状态变化?
状态不会自己变。它需要「事件」来推动。事件就是那些让状态发生转移的「外部刺激」。
在交易所协议里,事件通常来自三个方向:
- 用户操作:你发了一个撤单指令
- 交易所回报:成交回报、拒绝回报、状态变更通知
- 系统内部:超时、重连、心跳超时
我曾经犯过一个低级错误:把「撤单指令发出」和「撤单确认收到」当成同一个事件处理。结果呢?撤单指令发出后,订单状态直接跳到了「已撤销」,但交易所那边其实还没处理完。紧接着成交回报来了,状态机直接崩溃——因为「已撤销」状态下不应该收到成交回报。
事件的定义通常包含两个要素:事件类型和事件数据。比如:
class Event:
def __init__(self, event_type, data=None):
self.type = event_type # 'ORDER_ACK', 'FILL_REPORT', 'CANCEL_CONFIRM'
self.data = data # 携带的额外信息,比如成交价格、数量
3. 状态转移表——把规则写死
状态转移表,就是一张「什么状态下,遇到什么事件,应该变成什么状态」的对照表。它把所有的可能性都列出来,不留死角。
我个人习惯用表格来设计状态机,因为表格比代码更直观,方便团队评审。下面是一个简化的订单状态转移表:
| 当前状态 | 事件 | 下一状态 | 动作 |
|---|---|---|---|
| INIT | ORDER_SEND | PENDING | 发送订单到交易所 |
| PENDING | ORDER_ACK | ACTIVE | 记录订单ID |
| PENDING | ORDER_REJECT | CANCELLED | 记录拒绝原因 |
| ACTIVE | FILL_REPORT | PARTIAL_FILLED | 更新已成交数量 |
| ACTIVE | CANCEL_SEND | CANCELLING | 发送撤单指令 |
| PARTIAL_FILLED | FILL_REPORT | FILLED | 检查是否全部成交 |
| PARTIAL_FILLED | CANCEL_SEND | CANCELLING | 发送撤单指令 |
| CANCELLING | CANCEL_CONFIRM | CANCELLED | 释放资源 |
| CANCELLING | FILL_REPORT | PARTIAL_FILLED | 撤单前最后成交 |
4. 状态转移图——一图胜千言
表格虽然严谨,但不够直观。状态转移图就是用来解决这个问题的。它用图形化的方式展示状态之间的流转关系,一眼就能看出整个流程。
下面我用SVG画一个简化的订单状态转移图,展示核心的流转路径:
你看这张图,从INIT出发,经过ORDER_SEND事件到PENDING,再到ACTIVE,然后根据成交情况走不同的分支。整个流程一目了然。
5. 代码实现——把理论落地
理论讲完了,咱们看看代码怎么写。下面是一个极简的状态机框架,用Python实现:
class OrderStateMachine:
def __init__(self):
self.state = 'INIT'
# 状态转移表:{(当前状态, 事件): (下一状态, 回调函数)}
self.transitions = {
('INIT', 'ORDER_SEND'): ('PENDING', self.on_send),
('PENDING', 'ORDER_ACK'): ('ACTIVE', self.on_ack),
('PENDING', 'ORDER_REJECT'): ('CANCELLED', self.on_reject),
('ACTIVE', 'FILL_REPORT'): ('PARTIAL_FILLED', self.on_fill),
('ACTIVE', 'CANCEL_SEND'): ('CANCELLING', self.on_cancel_send),
('PARTIAL_FILLED', 'FILL_REPORT'): ('FILLED', self.on_fill),
('PARTIAL_FILLED', 'CANCEL_SEND'): ('CANCELLING', self.on_cancel_send),
('CANCELLING', 'CANCEL_CONFIRM'): ('CANCELLED', self.on_cancel_confirm),
('CANCELLING', 'FILL_REPORT'): ('PARTIAL_FILLED', self.on_fill),
}
def handle_event(self, event):
key = (self.state, event.type)
if key not in self.transitions:
raise ValueError(f"非法转移: 状态={self.state}, 事件={event.type}")
next_state, callback = self.transitions[key]
callback(event.data) # 执行动作
self.state = next_state
print(f"状态变更: {key[0]} -> {next_state}")
def on_send(self, data):
print("发送订单到交易所...")
def on_ack(self, data):
print(f"订单确认,ID={data}")
def on_reject(self, data):
print(f"订单被拒绝,原因={data}")
def on_fill(self, data):
print(f"成交回报: 价格={data.get('price')}, 数量={data.get('qty')}")
def on_cancel_send(self, data):
print("发送撤单指令...")
def on_cancel_confirm(self, data):
print("撤单确认")
这段代码的核心就是那个transitions字典。它把状态转移表直接映射成了代码。每次事件来了,查表、执行回调、更新状态,三步搞定。
6. 总结一下
状态机这东西,说白了就是「在什么状态下,遇到什么事,做什么动作,变成什么状态」。你只要把这四个问题回答清楚,状态机就设计好了。
我个人觉得,状态机最难的不是技术实现,而是「把所有可能的情况都想全」。很多线上事故,都是因为某个边界情况没考虑到。所以,设计状态机的时候,多花点时间在状态转移表上,把每个格子都填满,别留空白。
嗯,今天就聊到这儿。记住:状态机是协议解析的骨架,骨架稳了,上面的肉才好长。