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 撮合引擎触发条件
撮合引擎什么时候启动?这个问题看似简单,但不同场景差别很大。
我归纳了三种触发模式:
- 事件驱动:有新的吃单进来,立即触发撮合。这是最常用的模式。
- 定时轮询:每隔几毫秒检查一次订单簿,看有没有可撮合的订单。适合低延迟场景。
- 混合模式:平时事件驱动,高峰期切到定时轮询,防止事件风暴。
我个人更倾向事件驱动。为什么?因为定时轮询会有空转,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个都到了再撮合?我遇到过这种情况,如果逐个撮合,可能会出现价格滑点。后来我加了个「批量模式」,等一批订单全部到达后再统一撮合,效果好了很多。
注意:撮合引擎的触发条件里,一定要加防重入保护。我曾经因为没加,导致撮合函数递归调用,最后栈溢出了。加个标志位或者用状态机控制,能省很多麻烦。
知识体系总览
下面这张图,是我对本章核心逻辑的总结。你可以看到,四个操作之间是环环相扣的:
这张图里,你可以看到:插入和修改都可能触发撮合,撤销则通过状态机与撮合交互。四个操作不是孤立的,它们共享同一个订单簿数据结构。所以,设计时一定要考虑并发安全。
好了,这一章的内容就到这里。记住一句话:订单簿的核心,不是数据结构有多花哨,而是操作之间的协作有多默契。