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 | 订单创建时间戳(纳秒级) |
代码实现上,我习惯用结构体来定义:
// 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队列
}
你想想看,当一笔买单进来时,引擎会先看卖盘里有没有价格小于等于买单价格的订单。如果有,就按价格从低到高、时间从早到晚的顺序依次成交。这就是“价格优先、时间优先”原则。
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 成交记录
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,方便审计和回滚
- 市价单不进订单簿,直接成交
嗯,数据结构这块就聊到这儿。这些设计都是我踩过坑之后总结出来的,你直接拿去用,能少走不少弯路。下一章我们开始写真正的撮合逻辑,到时候这些数据结构就会活起来。