第二章:订单生命周期全景
订单从创建到成交或取消,这条路到底怎么走?
我做了这么多年做市商系统,发现很多新手容易把订单生命周期想得太简单。他们觉得:下单→成交,完事。其实这里面门道多着呢。今天我就把这条完整路径给你拆开揉碎了讲清楚。
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 性能与可靠性的平衡
做市商系统每秒处理成千上万笔订单,状态机不能成为瓶颈。我的做法是:
- 内存中维护一份「热状态」,用HashMap存着,O(1)查询
- 异步写数据库,保证最终一致性
- 定期做全量对账,发现不一致就告警
你想想看,如果每笔订单状态转换都写数据库,那延迟至少增加几毫秒。对于高频交易来说,这几毫秒可能就是生与死的区别。
好了,订单生命周期全景就讲到这里。记住一句话:状态机是订单系统的骨架,骨架歪了,肉长得再好也没用。
本章小结:订单从创建到成交/取消,经历了创建校验、状态机流转、原子性转换、异常处理等多个环节。每个环节都有坑,但只要你把状态机模型设计清楚,大部分问题都能提前规避。