第二章 订单生命周期:从下单到成交的完整流程

好,咱们直接进入正题。这一章我带你走一遍订单的完整旅程——从用户点击「买入」按钮,到最终成交记录落库。说白了,就是一笔订单在撮合引擎里到底经历了什么。

我个人习惯把订单生命周期分成四个阶段:接收→校验→撮合→结算。每个阶段都有坑,我一个个讲。

2.1 订单的诞生:接收与校验

用户下单,请求先到网关层。网关做两件事:格式校验风控检查

格式校验很简单——价格是不是正数?数量是不是大于0?交易对存不存在?这些要是不过,直接打回。

风控检查就有点意思了。我记得有一次,一个用户用API连续下了几百笔小单,每笔0.0001个BTC。单看每笔都没问题,但加起来明显是刷单行为。我们后来加了个「单位时间内下单频率」的检查,才把这问题按住。

核心原则:校验阶段宁可严,不可松。一个非法订单进入撮合核心,可能导致整个订单簿数据错乱。

2.2 订单状态机:一张图说清楚

订单状态机,说白了就是订单的「人生轨迹」。我直接给你看核心状态:

状态 含义 触发条件
NEW 新订单,等待撮合 校验通过后
PARTIALLY_FILLED 部分成交 撮合时只成交了一部分
FILLED 完全成交 订单全部成交
CANCELED 已撤销 用户主动撤单
REJECTED 被拒绝 校验失败

这里有个细节:PARTIALLY_FILLED 状态其实可以反复出现。比如你挂了一笔100个ETH的买单,市场先给你成交了30个,状态变成PARTIALLY_FILLED;过一会儿又成交了50个,状态还是PARTIALLY_FILLED;直到最后20个也成交了,才变成FILLED。

避坑指南:我曾经见过一个系统,PARTIALLY_FILLED状态只记录最后一次成交数量,导致用户看到的「已成交数量」永远不对。正确的做法是:每次成交都要累加已成交数量,同时更新剩余数量。

2.3 订单簿:撮合的核心战场

订单簿是什么?说白了就是一个「买家和卖家的报价清单」。买方的报价叫买单队列,卖方的叫卖单队列

我习惯用两个数据结构来表示:

// 买单队列:价格从高到低排序
buy_orders = [
  {price: 100.5, quantity: 10, order_id: "A001"},
  {price: 100.3, quantity: 5,  order_id: "A002"},
  {price: 100.1, quantity: 8,  order_id: "A003"}
]

// 卖单队列:价格从低到高排序
sell_orders = [
  {price: 100.6, quantity: 7,  order_id: "B001"},
  {price: 100.8, quantity: 12, order_id: "B002"},
  {price: 101.0, quantity: 3,  order_id: "B003"}
]

为什么买单要降序、卖单要升序?你想想看——买方当然想用最低价买,但能成交的永远是出价最高的买单;卖方想卖最高价,但能成交的永远是出价最低的卖单。这就是价格优先原则。

2.4 撮合流程:一步步拆解

假设现在来了一个买单:价格100.7,数量5个ETH。

第一步:检查卖单队列的最低价。当前最低卖价是100.6,低于100.7,可以成交。

第二步:成交。卖单B001有7个ETH,买单只要5个。所以成交5个,价格取100.6(卖方价格)。

第三步:更新状态。买单完全成交,变成FILLED。卖单B001还剩2个,状态变成PARTIALLY_FILLED。

嗯,这里要注意:成交价格到底取买方价还是卖方价?不同交易所规则不同。有的取卖方价,有的取买方价,还有的取两者中间价。我个人建议取先进入订单簿的那一方价格,这样逻辑最清晰。

重要提醒:撮合过程中,一定要保证原子性。要么全部成交,要么全部回滚。我曾经见过一个bug:成交了买单但没更新卖单的剩余数量,导致同一个ETH被卖了两次。这种问题在金融系统里是致命的。

2.5 结算:最后的收尾工作

成交之后,还有一堆事要做:

  • 更新账户余额:买方扣钱,卖方扣币
  • 生成成交记录:时间、价格、数量、买卖双方ID
  • 推送行情数据:最新价、成交量、深度变化
  • 通知用户:WebSocket推送成交回报

这里我踩过一个坑:结算顺序。是先更新余额,还是先生成成交记录?如果先更新余额,但生成记录时系统挂了,就会出现「钱扣了但没记录」的情况。正确的做法是:先持久化成交记录,再更新余额。这样即使后续步骤失败,也能通过记录做回滚。

2.6 完整流程的时间线

最后,我给你画个时间线,把整个流程串起来:

  1. T+0ms:用户下单,网关接收
  2. T+1ms:格式校验通过
  3. T+2ms:风控检查通过
  4. T+3ms:订单进入撮合引擎,状态设为NEW
  5. T+4ms:撮合引擎找到匹配的卖单,成交
  6. T+5ms:更新订单状态为FILLED/PARTIALLY_FILLED
  7. T+6ms:更新账户余额
  8. T+7ms:生成成交记录,写入数据库
  9. T+8ms:推送行情和成交回报

整个流程,从下单到用户收到成交通知,理想情况下应该在10毫秒以内完成。如果超过50毫秒,用户就能感觉到卡顿。

我的经验:性能瓶颈往往不在撮合逻辑本身,而在数据库写入和网络推送。我建议用异步写入——先返回成交结果给用户,再慢慢写数据库。当然,这需要做好数据一致性保障。

好了,订单生命周期就讲到这里。下一章我会深入订单簿的数据结构,讲讲怎么用跳表实现O(log n)级别的价格查询。到时候见。