3. 订单生命周期:订单状态机

订单状态机,说白了就是一笔订单从生到死的完整轨迹。我刚开始做撮合引擎时,觉得这东西不就是「新建→成交」嘛,有啥好设计的?结果第一次上线就出了大问题——部分成交的订单状态乱了,撤单和成交同时发生,数据库里一堆脏数据。嗯,从那以后我再也不敢小看状态机了。

3.1 订单的四种核心状态

我个人习惯把订单状态分成四类,简单明了:

状态 含义 典型场景
NEW 新建 用户刚提交订单,还没开始撮合
PARTIAL_FILLED 部分成交 订单被吃掉一部分,还剩一些挂在盘口
FILLED 完全成交 订单全部成交,生命周期结束
CANCELED 已撤销 用户主动撤单,或系统风控强制撤单

你可能会问:「那有没有其他状态?比如过期、异常?」
有的系统会加 EXPIRED、REJECTED 等状态。但核心流程里,这四个就够了。多了反而容易乱。

3.2 状态流转图

下面这张 SVG 图,是我自己画的状态流转逻辑。你看一眼就明白了:

NEW PARTIAL_FILLED FILLED CANCELED 撮合部分成交 继续撮合 剩余全部成交 一次性全部成交 用户撤单 部分撤单 实线:正常撮合流程 | 虚线:撤单流程 | 自环:部分成交后继续撮合

这张图我建议你存下来。每次设计状态机时,对着它检查一遍,基本不会漏掉流转路径。

3.3 状态流转的约束规则

状态不是随便跳的。我曾经在项目中遇到过一个问题:用户撤单请求和撮合成交同时到达,结果订单先被标记为 CANCELED,然后又变成了 FILLED。你说这算成交还是没成交?

所以,必须加约束:

  • NEW → 只能到 PARTIAL_FILLED、FILLED 或 CANCELED
  • PARTIAL_FILLED → 只能到 FILLED 或 CANCELED
  • FILLED → 终态,不能再变
  • CANCELED → 终态,不能再变

核心原则:一旦进入终态(FILLED 或 CANCELED),任何操作都不能再修改订单状态。这是保证数据一致性的底线。

3.4 代码实现示例

下面是我常用的状态机实现方式,用枚举 + 状态转移表:

// 订单状态枚举
public enum OrderStatus {
    NEW,
    PARTIAL_FILLED,
    FILLED,
    CANCELED
}

// 状态转移表
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new HashMap<>();

static {
    TRANSITIONS.put(OrderStatus.NEW, Set.of(
        OrderStatus.PARTIAL_FILLED,
        OrderStatus.FILLED,
        OrderStatus.CANCELED
    ));
    TRANSITIONS.put(OrderStatus.PARTIAL_FILLED, Set.of(
        OrderStatus.FILLED,
        OrderStatus.CANCELED
    ));
    TRANSITIONS.put(OrderStatus.FILLED, Collections.emptySet());
    TRANSITIONS.put(OrderStatus.CANCELED, Collections.emptySet());
}

// 状态变更方法
public boolean transitionTo(OrderStatus newStatus) {
    if (TRANSITIONS.get(this.status).contains(newStatus)) {
        this.status = newStatus;
        return true;
    }
    // 我曾经在这里加过告警日志,线上排查问题特别好用
    log.warn("非法状态转移: {} -> {}", this.status, newStatus);
    return false;
}

避坑指南:我曾经在状态变更时没加乐观锁,结果并发场景下同一笔订单被撮合了两次。后来在数据库里加了 version 字段,每次更新时检查 version,才彻底解决。

3.5 状态机设计要点

总结几个我踩过的坑,你直接拿来用:

  1. 状态变更必须原子化——用数据库事务或分布式锁,别让状态和实际成交数量不一致
  2. 记录状态变更日志——我习惯把每次状态变化都写到一张 order_status_log 表里,排查问题时直接看日志,不用猜
  3. 终态不可逆——FILLED 和 CANCELED 就是终点,任何业务逻辑都不能再改
  4. 考虑并发场景——撮合引擎是高并发系统,状态机必须能处理同时到来的成交和撤单请求

特别注意:部分成交状态下撤单,不是把整个订单都撤销,而是撤销剩余未成交的部分。已成交的部分不能动。这个逻辑很多新手会搞错。

好了,订单状态机就讲到这里。你想想看,其实核心就是四个状态、几条流转规则。但就是这看似简单的东西,决定了整个撮合引擎的数据一致性。我见过太多系统因为状态机设计不严谨,导致对账不平、资金损失。所以,别嫌我啰嗦,这块真的值得你花时间好好打磨。


专注资料整理