第二章:订单生命周期全景

订单从创建到成交或取消,这条路到底怎么走?

我做了这么多年做市商系统,发现很多新手容易把订单生命周期想得太简单。他们觉得:下单→成交,完事。其实这里面门道多着呢。今天我就把这条完整路径给你拆开揉碎了讲清楚。

2.1 订单的起点:创建与校验

订单的诞生,从客户端发起请求开始。但别以为发过来我就直接扔交易所了。我习惯在系统里设三道关卡:

  • 格式校验:价格、数量、方向这些字段对不对。我曾经遇到过有人把价格填成负数,系统直接崩了。
  • 风控校验:当前持仓够不够?资金余额足不足?日内亏损有没有超限?
  • 市场状态检查:现在是交易时间吗?这个品种允许交易吗?

这三关都过了,订单才会进入「待处理」状态。嗯,这里要注意,校验失败不是直接丢弃,而是生成一条拒绝记录,方便事后审计。

核心要点:订单创建阶段,校验比执行更重要。宁可多花5毫秒校验,也别让一笔坏单飞出去。

2.2 核心状态机模型

说白了,订单状态机就是一张「订单行为地图」。我把它画成了一张图,你一看就明白:

已创建 待成交 部分成交 全部成交 已取消 取消路径(任何状态均可触发) 图:订单核心状态机模型

这张图我画了好几个版本才定稿。你想想看,订单从「已创建」出发,要么一路走到「全部成交」,要么中途被取消。但「部分成交」这个状态特别有意思——它既是终点也是起点。

2.3 关键路径详解

咱们一条一条说:

路径 触发条件 典型耗时 我的经验
已创建→待成交 校验通过,进入订单簿 <1ms 这里最容易出问题的是并发校验
待成交→部分成交 市场出现对手盘,但未完全匹配 几ms到几小时 我见过挂了一周的部分成交单
部分成交→全部成交 剩余数量被市场消化 不确定 大单拆小单,这个阶段很关键
任何状态→已取消 用户撤单、超时、风控触发 即时 取消的优先级应该最高

避坑指南:我曾经在「部分成交→已取消」这条路径上栽过跟头。当时系统处理取消请求时,没检查是否还有未完成的成交回报,结果导致一笔订单既显示取消又显示成交。后来我加了个「取消中」的中间状态,才彻底解决这个问题。

2.4 状态转换的原子性

做市商系统里,状态转换必须是原子的。说白了,就是不能出现「半吊子」状态。我习惯用数据库事务来保证:

-- 伪代码示例:订单状态转换
BEGIN TRANSACTION;
    UPDATE orders 
    SET status = 'FILLED', 
        filled_qty = @filled_qty,
        update_time = NOW()
    WHERE order_id = @order_id 
    AND status = 'PARTIAL_FILLED';
    
    INSERT INTO order_fills (order_id, price, qty, time)
    VALUES (@order_id, @price, @qty, NOW());
COMMIT;

为什么这么写?因为如果先更新状态再插入成交记录,中间万一系统崩了,数据就不一致了。嗯,这里要注意,事务隔离级别最好用 READ COMMITTED,别用 SERIALIZABLE,不然性能扛不住。

2.5 异常场景处理

订单生命周期里,异常才是常态。我总结了几种常见情况:

  • 超时未成交:设置TTL(生存时间),到期自动取消。我一般设30秒,太短容易误杀,太长占用资源。
  • 重复成交回报:交易所可能发两次同样的成交消息。幂等性处理是关键,用订单号+成交序号做唯一键。
  • 状态回滚:比如订单显示已取消,但交易所又发来成交回报。这时候要人工介入,系统自动处理风险太大。

警告:千万不要在状态机里搞「万能转换」——即允许任意状态转到任意状态。我见过一个系统这么设计,结果订单状态乱成一锅粥。状态转换路径必须严格定义,每一条都要经过评审。

2.6 性能与可靠性的平衡

做市商系统每秒处理成千上万笔订单,状态机不能成为瓶颈。我的做法是:

  1. 内存中维护一份「热状态」,用HashMap存着,O(1)查询
  2. 异步写数据库,保证最终一致性
  3. 定期做全量对账,发现不一致就告警

你想想看,如果每笔订单状态转换都写数据库,那延迟至少增加几毫秒。对于高频交易来说,这几毫秒可能就是生与死的区别。

好了,订单生命周期全景就讲到这里。记住一句话:状态机是订单系统的骨架,骨架歪了,肉长得再好也没用。

本章小结:订单从创建到成交/取消,经历了创建校验、状态机流转、原子性转换、异常处理等多个环节。每个环节都有坑,但只要你把状态机模型设计清楚,大部分问题都能提前规避。

专注资料整理