第四章:执行引擎层设计
低延迟订单处理流水线、原子化操作设计、订单状态机——这三个东西,说白了就是执行引擎的骨架。
我做了这么多年量化系统,见过太多团队在策略层花了大把精力,结果执行层一塌糊涂。订单发不出去、状态对不上、延迟高得离谱。嗯,今天我们就来聊聊这块硬骨头。
4.1 低延迟订单处理流水线
先问个问题:你的订单从策略决策到发往交易所,中间经过了多少步骤?
我见过最夸张的,一个订单要经过七八个模块的序列化、反序列化、队列传递。你想想看,每一步都在吃延迟。高频交易里,微秒级的差距就是盈亏的分水岭。
核心原则:流水线设计要像工厂的传送带,每个工位只做一件事,做完立刻传给下一站。
我个人习惯把订单处理流水线拆成四个阶段:
- 接收阶段——从策略层拿到原始订单指令
- 校验阶段——检查资金、持仓、风控规则
- 转换阶段——把内部订单格式转成交易所协议
- 发送阶段——通过网卡直接发出去
每个阶段之间用无锁队列连接。为什么不用锁?因为锁会阻塞线程,阻塞就是延迟的元凶。我在项目中遇到过,一个简单的互斥锁没处理好,导致订单处理时间从5微秒飙到了50微秒。后来换成无锁的环形缓冲区,问题才解决。
// 伪代码:无锁环形缓冲区
struct LockFreeRingBuffer {
atomic<uint32_t> head;
atomic<uint32_t> tail;
OrderSlot slots[MAX_SLOTS];
bool push(Order& order) {
uint32_t t = tail.load(memory_order_relaxed);
uint32_t n = (t + 1) % MAX_SLOTS;
if (n == head.load(memory_order_acquire))
return false; // 满了
slots[t] = order;
tail.store(n, memory_order_release);
return true;
}
};
小技巧:流水线的每个阶段最好绑定到独立的CPU核心上,避免上下文切换。我一般会留一个核心专门做订单发送,其他核心别碰它。
4.2 原子化操作设计
什么叫原子化操作?就是一件事要么做完,要么没做,没有中间状态。
订单处理里最怕什么?最怕半路崩溃。比如你扣了资金,但订单没发出去。或者订单发出去了,但状态没更新。这种问题在分布式系统里特别常见。
我建议把每个操作都设计成原子单元。举个例子:
// 原子化下单操作
struct AtomicOrderOperation {
bool execute(Order& order) {
// 第一步:预扣资金(可回滚)
if (!reserveFunds(order)) {
return false;
}
// 第二步:发送订单(不可回滚)
if (!sendToExchange(order)) {
// 回滚资金
releaseFunds(order);
return false;
}
// 第三步:记录状态(可重试)
updateOrderStatus(order, OrderStatus::PENDING);
return true;
}
};
你看,每一步都有明确的失败处理逻辑。预扣资金失败了直接返回;发送失败了要回滚资金;状态更新失败了可以重试。这就是原子化的精髓——每个操作都有明确的边界和补偿机制。
注意:原子化不等于简单化。我曾经在一个项目里把原子操作粒度搞得太粗,一个操作里塞了太多逻辑,结果出了问题都不知道是哪一步失败的。后来拆成细粒度原子操作,每个操作只做一件事,排查问题快多了。
原子化操作还有一个好处:方便做幂等性。同一个订单重复执行,结果应该是一样的。交易所那边经常会有重复报单的情况,如果你的系统不幂等,那麻烦就大了。
4.3 订单状态机
订单状态机,说白了就是订单的一生。从出生到死亡,每个阶段都有明确的状态。
标准的状态流转是这样的:
New → Pending → Filled
→ Cancelled
→ Rejected
但实际项目中,状态远不止这些。我整理了一个更完整的版本:
| 状态 | 含义 | 可转换到 |
|---|---|---|
| New | 订单刚创建,还没处理 | Pending, Rejected |
| Pending | 已发往交易所,等待确认 | Filled, Cancelled, Rejected |
| PartiallyFilled | 部分成交 | Filled, Cancelled |
| Filled | 全部成交,终态 | 无 |
| Cancelled | 已撤销,终态 | 无 |
| Rejected | 被交易所拒绝,终态 | 无 |
状态机的实现,我推荐用状态模式或者查表法。查表法更直观:
// 状态转换表
struct StateTransition {
OrderState from;
OrderEvent event;
OrderState to;
};
StateTransition transitions[] = {
{New, SUBMIT, Pending},
{New, REJECT, Rejected},
{Pending, FILL, Filled},
{Pending, PARTIAL_FILL, PartiallyFilled},
{Pending, CANCEL, Cancelled},
{PartiallyFilled, FILL, Filled},
{PartiallyFilled, CANCEL, Cancelled},
};
关键点:状态转换必须是线程安全的。多个线程可能同时操作同一个订单的状态,不加锁的话状态就乱套了。我一般用原子变量加CAS操作来实现无锁状态转换。
嗯,这里要注意一个坑:交易所返回的状态和本地状态可能不一致。比如你发了撤单指令,但交易所那边已经成交了。这时候本地状态是Cancelled,但实际是Filled。怎么办?
我的做法是:以交易所的最终确认为准。本地状态只是参考,最终状态一定要等交易所的回包。我曾经因为这个吃过亏,本地显示撤单成功,结果第二天结算发现那笔单子成交了,亏了不少钱。
4.4 整体架构图
说了这么多,咱们画张图把整个执行引擎串起来:
这张图把整个执行引擎的脉络理清楚了。策略层下来的订单,经过无锁队列进入流水线,每个阶段都是原子操作,状态机全程跟踪订单的生命周期。最后通过发送阶段到达交易所。
避坑指南:我曾经在流水线里加了一个日志打印,结果延迟直接翻倍。后来把所有日志都改成异步写入,才把延迟降回来。记住,执行引擎里任何同步IO都是敌人。
好了,执行引擎层的内容就这些。核心就三件事:流水线要快、操作要原子、状态要清晰。把这三点做好了,你的交易系统就成功了一大半。