3. 订单生命周期管理:订单状态机设计

订单状态机,说白了就是订单的“生死簿”。

从它诞生(New)到最终消亡(Filled、Cancelled、Rejected),每一步都得有据可查。我见过不少团队,一开始觉得状态机简单,结果上线后各种状态“穿越”,订单丢了都不知道怎么丢的。

今天咱们就把这个核心模块拆开揉碎,讲清楚。

3.1 为什么需要状态机?

你想想看,一个订单从发出到成交,中间可能经历什么?

  • 刚发出去,交易所说“不行,格式错了”——Rejected
  • 交易所说“好的,正在处理”——New
  • 成交了一部分,还剩一点挂着——PartiallyFilled
  • 全成交了——Filled
  • 你突然想撤单——Cancelled

如果没有状态机,这些状态之间的转换就会乱套。比如一个已经Filled的订单,理论上不能再变成New。但代码里如果没做约束,就可能出现这种荒谬的事。

核心原则: 状态转换必须是单向的、确定的。不允许回退,不允许跳跃。

3.2 状态定义与转换规则

我个人习惯把订单状态分为两类:终态非终态

状态 类型 说明
New 非终态 订单已提交,等待交易所确认或成交
PartiallyFilled 非终态 部分成交,剩余部分仍在市场中等候
Filled 终态 全部成交,订单生命周期结束
Cancelled 终态 用户主动撤销,或系统因超时等原因撤销
Rejected 终态 交易所拒绝,订单无效

这里有个关键点:PartiallyFilled 是唯一一个可以多次进入的状态。比如一个大单,可能分10次成交,每次成交后状态还是PartiallyFilled,直到剩余量为0才变成Filled。

避坑指南: 我曾经遇到过一个case,订单已经PartiallyFilled了,但客户端又发来一个撤单指令。这时候不能直接变成Cancelled,得先检查剩余量。如果剩余量>0,可以撤单;如果剩余量=0,其实已经Filled了,撤单指令应该被忽略。

3.3 状态机核心逻辑

下面是我在实际项目中用过的状态机核心代码。嗯,这里要注意,状态机最好用枚举+状态转换表的方式实现,别用一堆if-else,否则维护起来会疯掉。

// 订单状态枚举
public enum OrderStatus {
    New,
    PartiallyFilled,
    Filled,
    Cancelled,
    Rejected
}

// 状态转换表
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITION_MAP = new HashMap<>() {{
    put(OrderStatus.New, new HashSet<>(Arrays.asList(
        OrderStatus.PartiallyFilled,
        OrderStatus.Filled,
        OrderStatus.Cancelled,
        OrderStatus.Rejected
    )));
    put(OrderStatus.PartiallyFilled, new HashSet<>(Arrays.asList(
        OrderStatus.PartiallyFilled,  // 允许继续部分成交
        OrderStatus.Filled,
        OrderStatus.Cancelled         // 剩余部分可以撤单
    )));
    // 终态不允许任何转换
    put(OrderStatus.Filled, Collections.emptySet());
    put(OrderStatus.Cancelled, Collections.emptySet());
    put(OrderStatus.Rejected, Collections.emptySet());
}};

public boolean canTransition(OrderStatus from, OrderStatus to) {
    Set<OrderStatus> allowed = TRANSITION_MAP.get(from);
    return allowed != null && allowed.contains(to);
}

这段代码看起来简单,但它是整个风控引擎的基石。我建议你把状态转换表做成可配置的,比如用YAML文件加载。这样以后要加新状态(比如“挂起”、“待重试”),改配置文件就行,不用动代码。

3.4 状态机流程图

光说理论不够直观,我画了一张状态机流转图,你看看就明白了。

New PartiallyFilled Filled Cancelled Rejected 部分成交 全部成交 撤单 拒绝 继续成交 剩余成交 撤剩余 非终态 终态

从图上你能看到:New是唯一的入口,所有订单都得先经过New。然后根据交易所的反馈,走向不同的分支。PartiallyFilled可以自循环,这是高频交易里最常见的场景——一个大单被拆成无数个小单慢慢吃。

3.5 实战中的坑与对策

理论讲完了,说点实际的。我在搭建风控引擎时,踩过几个坑,分享给你。

坑1:并发状态更新
同一个订单,可能同时收到成交回报和撤单回报。如果两个线程同时更新状态,就会出问题。
解决方案:用乐观锁或者单线程处理。我个人倾向用单线程的事件队列,把所有回报串行化处理。
坑2:状态持久化
系统崩溃后,内存里的状态全丢了。怎么办?
我建议每笔订单的状态变更都写WAL(Write-Ahead Log)。重启后从WAL恢复,保证状态不丢。
坑3:PartiallyFilled的剩余量计算
我曾经犯过一个低级错误:用浮点数计算剩余量。结果因为精度问题,明明成交完了,剩余量还显示0.0000001。
记住:所有金额和数量都用整数(最小单位),比如用“分”而不是“元”,用“股”而不是“手”。

3.6 状态机与风控的联动

状态机不只是记录状态,它还是风控的第一道防线

举个例子:如果一个订单状态是PartiallyFilled,风控系统会检查它的未成交部分是否超过了某个阈值。如果超过,系统会自动触发撤单。

再比如,Rejected状态出现后,风控系统会记录拒绝原因。如果连续3次因为“资金不足”被拒,系统会暂时冻结该账户的订单提交权限。

这些逻辑都建立在状态机准确无误的基础上。所以,先把状态机搞扎实,后面的风控策略才能放心往上堆。

总结一句话: 订单状态机是风控引擎的“骨架”,骨架歪了,肉长得再好也没用。

公众号:蓝海资料掘金营,微信deep3321