4、订单簿核心操作:订单插入、撤销、修改与撮合触发

好,咱们今天聊聊订单簿最核心的几个操作。说白了,一个订单系统能不能扛住高并发,就看这几个操作写得够不够硬。我这些年踩过的坑,十有八九都跟这些基础操作有关。

4.1 订单插入逻辑

插入订单,听起来简单吧?不就是往数据结构里塞一条记录吗?嗯,这里要注意——插入的时机和位置,决定了撮合引擎能不能高效工作。

我个人习惯把订单分成两类:吃单挂单。吃单进来,直接去撮合;挂单进来,才真正执行插入。

核心原则:插入时保持价格优先、时间优先的排序。买盘从高到低,卖盘从低到高。

举个例子,买盘队列里已经有三个价格:100.5、100.3、100.1。这时候一个新买单挂100.4,它应该插在100.5后面、100.3前面。我早期用数组做这个,每次插入都要O(n)的移动,后来改成跳表,性能才上去。

// 伪代码:插入买单到价格队列
function insertBuyOrder(order):
    priceLevel = findPriceLevel(order.price)
    if priceLevel == null:
        priceLevel = new PriceLevel(order.price)
        insertIntoBuyQueue(priceLevel)  // 按价格降序插入
    priceLevel.addOrder(order)          // 按时间顺序追加

避坑指南:我曾经在插入时忘记检查价格是否已经存在,结果同一个价格创建了两个level,撮合时价格优先逻辑直接乱掉。记得先查再插。

4.2 订单撤销逻辑

撤销操作,看起来就是删一条记录。但实际项目中,撤销的复杂度往往被低估。

为什么?因为订单可能处于不同状态:已部分成交完全未成交正在撮合中。撤销时,你得区分对待。

  • 完全未成交:直接从价格队列移除,干净利落
  • 部分成交:只撤销剩余未成交部分,已成交的不能动
  • 正在撮合中:加个撤销标记,等撮合完再处理

我记得有一次线上事故,就是因为没处理「正在撮合中」的撤销。一个订单被撮合了,同时又被撤销了,结果数据库里出现了负数的成交量。嗯,从那以后我加了个状态机。

// 伪代码:撤销订单
function cancelOrder(orderId):
    order = getOrder(orderId)
    if order.status == PARTIAL_FILLED:
        order.remainingQuantity = 0
        removeFromPriceLevel(order)
        // 已成交部分保持不变
    else if order.status == PENDING:
        removeFromPriceLevel(order)
        order.status = CANCELLED
    else if order.status == MATCHING:
        order.cancelFlag = true  // 延迟处理

注意:撤销操作一定要加锁,但锁的粒度要小。我见过有人锁整个订单簿,结果撤销一个订单,所有撮合都卡住了。建议只锁对应的价格level。

4.3 订单修改逻辑(改价/改量)

修改操作,其实是个「伪操作」。你想想看,订单在队列里的位置是由价格决定的。改了价格,位置就得变。改了数量,可能影响撮合优先级。

所以我的做法很简单:先撤销,再插入。别想着原地修改,那会引入太多边界情况。

修改类型 处理方式 注意事项
改价 撤销原订单,按新价格重新插入 新价格可能触发撮合
加量 撤销原订单,按新数量重新插入 时间优先级会丢失
减量 直接修改剩余数量 不能低于已成交部分

这里有个坑:改价时,新价格如果跟对手盘有交叉,要不要立即撮合?我个人建议是。因为用户改价的目的就是希望更快成交,你不撮合,用户反而会觉得系统反应慢。

我的经验:改量时,如果只是减量,可以原地修改,不用重新排队。但加量不行,因为加量相当于新增了一部分订单,按规矩得排到队尾。这个细节,很多新手会忽略。

4.4 撮合引擎触发条件

撮合引擎什么时候启动?这个问题看似简单,但不同场景差别很大。

我归纳了三种触发模式:

  1. 事件驱动:有新的吃单进来,立即触发撮合。这是最常用的模式。
  2. 定时轮询:每隔几毫秒检查一次订单簿,看有没有可撮合的订单。适合低延迟场景。
  3. 混合模式:平时事件驱动,高峰期切到定时轮询,防止事件风暴。

我个人更倾向事件驱动。为什么?因为定时轮询会有空转,CPU浪费在无意义的检查上。你想想看,如果市场很冷清,一秒都没一个新订单,你还在那每秒轮询1000次,不是白费力气吗?

// 伪代码:事件驱动的撮合触发
function onNewOrder(order):
    if order.side == BUY:
        while order.remainingQuantity > 0:
            bestAsk = getBestAsk()
            if bestAsk == null || bestAsk.price > order.price:
                break  // 没有可撮合的卖单
            match(order, bestAsk)
    else:
        // 卖单同理

关键点:撮合触发后,要检查是否产生了新的最优价格。如果最优价格变了,需要推送行情更新。这个推送的时机,我建议放在撮合完成之后、返回结果之前。

还有一个容易被忽略的点:批量订单。比如用户一次性下了10个订单,你是每来一个撮合一次,还是等10个都到了再撮合?我遇到过这种情况,如果逐个撮合,可能会出现价格滑点。后来我加了个「批量模式」,等一批订单全部到达后再统一撮合,效果好了很多。

注意:撮合引擎的触发条件里,一定要加防重入保护。我曾经因为没加,导致撮合函数递归调用,最后栈溢出了。加个标志位或者用状态机控制,能省很多麻烦。

知识体系总览

下面这张图,是我对本章核心逻辑的总结。你可以看到,四个操作之间是环环相扣的:

订单簿核心操作流程 订单插入 价格优先·时间优先 订单撤销 状态机控制 订单修改 先撤后插 撮合引擎 事件驱动 / 定时轮询 吃单触发 撤销标记 改价可能触发 状态流转 成交结果 & 行情推送 四个操作环环相扣,任何一个出问题都会影响全局

这张图里,你可以看到:插入和修改都可能触发撮合,撤销则通过状态机与撮合交互。四个操作不是孤立的,它们共享同一个订单簿数据结构。所以,设计时一定要考虑并发安全。

好了,这一章的内容就到这里。记住一句话:订单簿的核心,不是数据结构有多花哨,而是操作之间的协作有多默契