2、核心数据结构设计:订单薄(OrderBook)的设计、订单(Order)的数据结构、成交(Trade)的数据结构

好,咱们直接进入正题。上一章我们聊了撮合引擎的整体架构,今天要啃的这块骨头,是引擎的“骨架”——核心数据结构。说白了,就是订单长什么样、订单簿怎么组织、成交记录怎么存。

我个人习惯,在设计任何系统之前,先把数据结构定死。数据结构定好了,后面的逻辑实现就是水到渠成的事。你想想看,如果订单字段今天加一个、明天改一个,撮合逻辑得跟着改多少遍?

2.1 订单(Order)的数据结构

订单是撮合引擎里最基础的数据单元。它代表了一个交易者的买卖意愿。嗯,这里要注意,订单不仅仅是“谁要买什么”这么简单,它还包含了价格、数量、时间、状态等一系列信息。

我在项目中遇到过最头疼的问题,就是订单状态管理混乱。一个订单被重复成交,或者撤单后又被撮合,都是因为状态字段设计得不够严谨。

核心字段设计:
字段名 类型 说明
order_id uint64 全局唯一订单ID,通常用雪花算法生成
user_id uint64 用户ID,用于归属和风控
symbol string 交易对,比如 "BTCUSDT"
side enum 买卖方向:BUY 或 SELL
type enum 订单类型:LIMIT(限价)、MARKET(市价)
price int64 价格,用整数表示,避免浮点数精度问题
quantity int64 原始数量
filled_quantity int64 已成交数量
status enum 状态:PENDING、PARTIAL_FILLED、FILLED、CANCELED
timestamp int64 订单创建时间戳(纳秒级)
避坑指南: 我曾经把价格和数量设计成double类型,结果在撮合比较时出现了精度问题,导致一笔大单的成交价差了0.0001个BTC。后来全部改用int64,价格乘以10^8存储,彻底解决了这个问题。

代码实现上,我习惯用结构体来定义:

// Order 订单结构体
type Order struct {
    OrderID       uint64  `json:"order_id"`
    UserID        uint64  `json:"user_id"`
    Symbol        string  `json:"symbol"`
    Side          Side    `json:"side"`          // BUY or SELL
    Type          OrderType `json:"type"`        // LIMIT or MARKET
    Price         int64   `json:"price"`         // 价格 * 10^8
    Quantity      int64   `json:"quantity"`      // 原始数量 * 10^8
    FilledQuantity int64  `json:"filled_quantity"`
    Status        OrderStatus `json:"status"`
    Timestamp     int64   `json:"timestamp"`     // 纳秒时间戳
}

2.2 订单簿(OrderBook)的设计

订单簿是撮合引擎的核心。它维护着所有未成交的限价订单。说白了,就是买家和卖家各自排着队,等着对方来成交。

我见过不少新手把订单簿设计成一个简单的数组,然后每次撮合都遍历一遍。这种做法在订单量少的时候还行,一旦到了每秒几万笔订单,性能就直接崩了。

正确的做法是什么?用“价格队列”+“跳表/红黑树”的组合。每个价格点对应一个订单队列,然后按价格排序。

订单簿的核心结构:
  • 买盘(Bids):按价格从高到低排序。谁出价高,谁优先成交。
  • 卖盘(Asks):按价格从低到高排序。谁卖得便宜,谁优先成交。
  • 价格队列(PriceLevel):同一个价格的所有订单,按时间顺序排队(FIFO)。

这里有个关键点:为什么用跳表而不是红黑树?我个人习惯用跳表,因为实现简单,而且并发控制更容易。红黑树虽然查找快,但平衡操作太复杂,在高并发场景下容易出问题。

// OrderBook 订单簿
type OrderBook struct {
    Symbol string
    Bids   *SkipList  // 买盘,价格降序
    Asks   *SkipList  // 卖盘,价格升序
}

// PriceLevel 价格队列
type PriceLevel struct {
    Price  int64
    Orders *Queue  // FIFO队列
}

你想想看,当一笔买单进来时,引擎会先看卖盘里有没有价格小于等于买单价格的订单。如果有,就按价格从低到高、时间从早到晚的顺序依次成交。这就是“价格优先、时间优先”原则。

注意: 市价单(MARKET)不进入订单簿。它直接跟订单簿里的对手盘成交,成交不完的部分就立即撤销。我曾经见过一个系统把市价单也放进订单簿,结果导致价格严重偏离,被套利机器人薅走了几十个BTC。

2.3 成交(Trade)的数据结构

成交记录是撮合引擎的“产出品”。每一笔成交,都代表着一笔买卖双方达成一致。成交记录不仅要用于清算,还要用于行情推送、K线生成、风控审计等。

我记得有一次,交易所被黑客攻击,需要回滚交易数据。就是因为成交记录的ID设计得不够严谨,导致回滚时无法准确找到对应的订单。从那以后,我对成交ID的设计就格外小心。

字段名 类型 说明
trade_id uint64 全局唯一成交ID,严格递增
buy_order_id uint64 买单ID
sell_order_id uint64 卖单ID
price int64 成交价格
quantity int64 成交数量
timestamp int64 成交时间戳
taker_side enum 吃单方:BUY 或 SELL
小技巧: trade_id 我建议用数据库自增ID或者全局序列号生成器,保证严格递增。这样在查询“某笔成交之后的所有成交”时,直接用ID范围查询,性能极佳。
// Trade 成交记录
type Trade struct {
    TradeID     uint64 `json:"trade_id"`
    BuyOrderID  uint64 `json:"buy_order_id"`
    SellOrderID uint64 `json:"sell_order_id"`
    Price       int64  `json:"price"`
    Quantity    int64  `json:"quantity"`
    Timestamp   int64  `json:"timestamp"`
    TakerSide   Side   `json:"taker_side"` // 谁是主动吃单方
}

2.4 核心流程:订单如何变成成交?

光讲数据结构有点干,咱们结合流程来看。下面这张图,是我用SVG画的订单簿撮合核心流程,你一看就明白。

订单簿撮合核心流程 新订单到达 市价单? 直接与对手盘成交 检查能否成交 能成交? 逐笔撮合 加入订单簿 生成成交记录 成交推送 + 订单簿更新

流程其实不复杂。新订单进来,先判断是市价单还是限价单。市价单直接跟对手盘干,干不完就撤。限价单先看看能不能跟现有订单成交,能成交就逐笔撮合,不能成交就扔进订单簿排队。

这里有个细节:逐笔撮合时,每次成交都要更新两个订单的已成交数量。如果订单完全成交了,就从订单簿里移除。如果只成交了一部分,订单还在,但数量减少了。

总结一下今天的内容:
  • 订单结构要包含状态字段,避免重复成交
  • 订单簿用价格队列+跳表实现,性能最优
  • 成交记录要严格递增ID,方便审计和回滚
  • 市价单不进订单簿,直接成交

嗯,数据结构这块就聊到这儿。这些设计都是我踩过坑之后总结出来的,你直接拿去用,能少走不少弯路。下一章我们开始写真正的撮合逻辑,到时候这些数据结构就会活起来。


专注资料整理