前言
寫這篇文章的出發點很簡單:把"撮合系統(Matching Engine)"這件事講清楚,讓不熟交易所內部的人也能讀得懂。我做過撮合相關的開發和優化,下面用工程視角把它的職責、核心規則、常見實現思路和工程難點講明白,儘量用生活化的例子和實際場景來説明,方便直接拿去公眾號發佈。
一、一句話定義
撮合系統就是把市場上買單和賣單按照規則配對併產生成交記錄的核心引擎。它決定了誰以什麼價、什麼量成交,並維護着持續變化的訂單簿(Order Book)。
二、撮合系統主要做什麼
- 接收訂單:各種類型的訂單(限價單、市場單、撤單、部分成交回報等)
- 校驗與風控:檢查參數合法性、賬户資金/持倉凍結、風控規則(速率、單筆限額等)
- 執行撮合規則:按照價格優先、時間優先等規則把買賣撮合成成交
- 產生成交事件:寫成交記錄、回報下單方、更新賬户餘額與倉位
- 更新訂單簿:把未成交的限價單放到訂單簿裏,作為未來撮合的對手
- 持久化與廣播:寫日誌、持久化訂單/成交、廣播行情和委託回報
三、核心規則:價格優先、時間優先
這是絕大多數交易所撮合的基石。
價格優先:買單以更高價格優先成交;賣單以更低價格優先成交。買方願意出更高價,理應優先拿到流動性;賣方願意接受更低價,理應優先被撮合。
時間優先:同一價格上的多個委託,先到先撮合(FIFO)。同價位裏先掛單的人享有優先成交權。
舉個簡單例子:當前賣盤有兩個賣單,A賣100@10:00、B賣50@10:01;此時來一單買150@11:00(限價100),撮合結果是先和A成交100,再和B成交50;如果買單隻買120,則對A成交100後還會對B成交20(部分成交),剩餘B掛單數量減少。
四、常見訂單類型和處理
限價單(Limit Order):指定價格,未成交則掛在簿上等待對手。限價單通常是"被動單(maker)"。
市價單(Market Order):按當前市場最優價儘快成交,不保留價格,撮合到掛單被吃完為止(若流動性不足,剩餘可能被拒絕或視為失敗)。
IOC/FOK等附加屬性:立即成交或取消(IOC)、全額成交或取消(FOK)等。
撤單:用户請求撤銷未成交部分,系統要能快速從訂單簿中刪除對應掛單並回報。
五、成交價的常見規則
成交價的取值有兩種常見策略:
被動價(常見):以被動掛單的價格作為成交價,例如市面上大多數撮合遵循"取掛單價"。
雙邊價/撮合價:在某些競價或集合競價場景,成交價可能由最大成交量或其它規則決定(例如開盤集合競價)。
六、撮合系統的幾種典型架構思路(工程角度)
單線程撮合器:把所有訂單按隊列順序交到單線程裏處理,優點是順序性強、實現簡單,不用考慮併發一致性;缺點是吞吐受限,但對於單個交易對常常足夠且延遲可控。
分片/多線程:對不同交易對做分片,各自單線程處理,擴展性好,但要做好路由和一致性;對極高吞吐的單一交易對,還可能對價格區間或用户做更細粒度的分片。
隊列化入口 + 寫前日誌(WAL):所有外部請求先寫日誌,然後入撮合隊列,保證可恢復性;撮合器處理完再寫確認/回報。
內存數據結構優化:訂單簿通常按價格做有序結構(例如跳錶、TreeMap或自定義數組結構),每個價位維護FIFO隊列。生產環境會極盡優化以減少GC和內存抖動。
七、工程難點與優化方向
延遲與吞吐:撮合延遲直接影響撮合結果和用户體驗,生產系統常做極致的延遲優化(如避免鎖、減少對象分配、使用內存池、JVM調優或寫C++/Rust版本)。
併發與一致性:如何在保證撮合順序的同時實現高併發接入,是系統設計的核心挑戰之一。
持久化與恢復:崩潰恢復需要能從交易日誌重放到某一時刻,確保賬户與訂單狀態一致。
風控與防攻擊:防止刷單、閃電撤單、異常流量;撮合前的餘額凍結、下單速率限制、黑名單等都很重要。
測試覆蓋:單元測試、壓力測試、回放測試(用歷史訂單回放檢查一致性)、混沌測試(故障注入)不可或缺。
八、常見運維和監控點
- 延遲分佈(P50/P99/P999)和每秒成交量(TPS)
- 訂單簿深度與掛單量,價差(Spread)變化
- 日誌寫入錯誤、持久化滯後、隊列堆積
- 風險告警:異常撤單率、異常撮合失敗率、用户資金不一致告警
九、説幾句實踐建議
如果只是起步:先用單線程、明確的撮合規則、完善的測試和日誌,再在瓶頸處做優化。先正確後做快。
關注可恢復性:總是把寫前日誌(WAL)和冪等回放做好,系統一旦崩潰能重建狀態是底線。
設計隔離:把撮合、賬務、風控、廣播模塊拆開,按接口通信,這樣更易演進和擴展。
多做場景測試:市場突然斷貨、大量市價單涌入、網絡抖動、機器重啓……這些都是線上常見災難場景。
結語
撮合系統看起來是"撮合買賣",但工程實現裏涉及到併發、延遲、持久化、風控和大量測試。把基本規則(價格優先、時間優先)弄清楚,再逐步把性能和可靠性打磨好,是把一台可上線的撮合引擎交付的正確路徑。如果你想把某一部分講得更細(比如撮合器的線程模型、訂單簿數據結構比較、或如何做高併發下的撤單優化),告訴我你關心的點,我可以把那一塊拆成專門的技巧篇。
附完整示例代碼
package com.lp;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.TreeMap;
/**
* 訂單方向枚舉
* BUY: 買單方向
* SELL: 賣單方向
*/
enum Direction {
BUY,
SELL
}
/**
* 訂單實體類
* 表示一個完整的交易訂單,包含訂單的所有屬性
*/
class OrderEntity {
/** 訂單唯一序列號,用於時間優先排序 */
public final long sequenceId;
/** 訂單方向:BUY或SELL */
public final Direction direction;
/** 訂單價格,使用BigDecimal確保精確計算 */
public final BigDecimal price;
/** 訂單數量,剩餘可成交數量 */
public long quantity;
/** 訂單創建時間戳 */
public final long timestamp;
/**
* 構造訂單實體
* @param sequenceId 訂單唯一序列號
* @param direction 訂單方向
* @param price 訂單價格
* @param quantity 訂單數量
*/
public OrderEntity(long sequenceId, Direction direction, BigDecimal price, long quantity) {
this.sequenceId = sequenceId;
this.direction = direction;
this.price = price;
this.quantity = quantity;
this.timestamp = System.currentTimeMillis();
}
/**
* 重寫toString方法,用於打印訂單信息
* @return 格式化的訂單字符串
*/
@Override
public String toString() {
return String.format("Order{id=%d, dir=%s, price=%s, qty=%d}",
sequenceId, direction, price, quantity);
}
}
/**
* 訂單鍵記錄類
* 用於訂單簿中的排序,包含序列號和價格兩個關鍵字段
* @param sequenceId 訂單序列號
* @param price 訂單價格
*/
record OrderKey(long sequenceId, BigDecimal price) {
// 注意:使用record自動生成equals和hashCode方法,確保TreeMap能正確定位訂單
}
/**
* 訂單簿類
* 管理同一方向的所有訂單,實現訂單的排序、添加、移除和查詢
*/
class OrderBook {
/** 訂單簿方向:BUY或SELL */
public final Direction direction;
/**
* 訂單存儲的核心數據結構
* 使用TreeMap實現O(logN)的插入、刪除和查詢效率
* 排序規則由訂單方向決定
*/
public final TreeMap<OrderKey, OrderEntity> book;
/**
* 賣盤排序比較器
* 規則:1. 價格從低到高;2. 價格相同時,序列號小的優先(時間優先)
*/
private static final Comparator<OrderKey> SORT_SELL = (o1, o2) -> {
// 價格比較:低價格在前
int priceCmp = o1.price().compareTo(o2.price());
// 價格相同時,序列號比較:小序列號在前(時間優先)
return priceCmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : priceCmp;
};
/**
* 買盤排序比較器
* 規則:1. 價格從高到低;2. 價格相同時,序列號小的優先(時間優先)
*/
private static final Comparator<OrderKey> SORT_BUY = (o1, o2) -> {
// 價格比較:高價格在前(注意o2和o1的順序)
int priceCmp = o2.price().compareTo(o1.price());
// 價格相同時,序列號比較:小序列號在前(時間優先)
return priceCmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : priceCmp;
};
/**
* 構造訂單簿
* @param direction 訂單簿方向
*/
public OrderBook(Direction direction) {
this.direction = direction;
// 根據訂單方向選擇對應的排序比較器
this.book = new TreeMap<>(direction == Direction.BUY ? SORT_BUY : SORT_SELL);
}
/**
* 獲取最優價格訂單
* 買盤返回價格最高的訂單,賣盤返回價格最低的訂單
* @return 最優價格訂單,若訂單簿為空則返回null
*/
public OrderEntity getFirst() {
return this.book.isEmpty() ? null : this.book.firstEntry().getValue();
}
/**
* 從訂單簿中移除指定訂單
* @param order 要移除的訂單
* @return 移除成功返回true,否則返回false
*/
public boolean remove(OrderEntity order) {
// 使用訂單的序列號和價格構建OrderKey,用於在TreeMap中定位
return this.book.remove(new OrderKey(order.sequenceId, order.price)) != null;
}
/**
* 向訂單簿中添加訂單
* @param order 要添加的訂單
* @return 添加成功返回true,若訂單已存在則返回false
*/
public boolean add(OrderEntity order) {
// 使用訂單的序列號和價格構建OrderKey,TreeMap會自動根據比較器排序
return this.book.put(new OrderKey(order.sequenceId, order.price), order) == null;
}
/**
* 重寫toString方法,用於打印訂單簿狀態
* @return 格式化的訂單簿字符串
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(direction).append(" Book:\n");
for (OrderEntity order : book.values()) {
sb.append(" ").append(order).append("\n");
}
return sb.toString();
}
}
/**
* 成交記錄類
* 表示一次成功的撮合交易,包含成交的關鍵信息
*/
class Trade {
/** 成交的買單ID */
public final long buyOrderId;
/** 成交的賣單ID */
public final long sellOrderId;
/** 成交價格 */
public final BigDecimal price;
/** 成交數量 */
public final long quantity;
/** 成交時間戳 */
public final long timestamp;
/**
* 構造成交記錄
* @param buyOrderId 買單ID
* @param sellOrderId 賣單ID
* @param price 成交價格
* @param quantity 成交數量
*/
public Trade(long buyOrderId, long sellOrderId, BigDecimal price, long quantity) {
this.buyOrderId = buyOrderId;
this.sellOrderId = sellOrderId;
this.price = price;
this.quantity = quantity;
this.timestamp = System.currentTimeMillis();
}
/**
* 重寫toString方法,用於打印成交信息
* @return 格式化的成交字符串
*/
@Override
public String toString() {
return String.format("Trade{buyId=%d, sellId=%d, price=%s, qty=%d}",
buyOrderId, sellOrderId, price, quantity);
}
}
/**
* 撮合引擎核心類
* 實現買賣訂單的自動撮合邏輯,遵循價格優先、時間優先的原則
*/
public class MatchEngine {
/** 買盤訂單簿,管理所有未成交的買單 */
private final OrderBook buyBook;
/** 賣盤訂單簿,管理所有未成交的賣單 */
private final OrderBook sellBook;
/** 訂單序列號計數器,用於生成唯一的訂單ID */
private long sequenceCounter = 0;
/**
* 構造撮合引擎
* 初始化買盤和賣盤訂單簿
*/
public MatchEngine() {
this.buyBook = new OrderBook(Direction.BUY);
this.sellBook = new OrderBook(Direction.SELL);
}
/**
* 生成唯一的訂單序列號
* 使用synchronized確保線程安全
* @return 下一個唯一序列號
*/
private synchronized long nextSequenceId() {
return ++sequenceCounter;
}
/**
* 提交新訂單到撮合引擎
* @param direction 訂單方向
* @param price 訂單價格
* @param quantity 訂單數量
*/
public void submitOrder(Direction direction, BigDecimal price, long quantity) {
// 創建新訂單,生成唯一序列號
OrderEntity newOrder = new OrderEntity(nextSequenceId(), direction, price, quantity);
System.out.println("\n=== 提交訂單: " + newOrder);
// 根據訂單方向執行不同的撮合邏輯
if (direction == Direction.BUY) {
matchBuyOrder(newOrder);
} else {
matchSellOrder(newOrder);
}
// 打印當前訂單簿狀態,便於觀察撮合結果
System.out.println(buyBook);
System.out.println(sellBook);
}
/**
* 撮合買單邏輯
* 嘗試將買單與賣盤訂單進行匹配,遵循價格優先、時間優先原則
* @param buyOrder 待撮合的買單
*/
private void matchBuyOrder(OrderEntity buyOrder) {
// 循環撮合,直到買單完全成交或無法匹配
while (buyOrder.quantity > 0) {
// 獲取賣盤中的最優賣單(價格最低的賣單)
OrderEntity bestSell = sellBook.getFirst();
// 情況1:賣盤為空,沒有可匹配的賣單
if (bestSell == null) {
// 將買單加入買盤,等待後續匹配
buyBook.add(buyOrder);
break;
}
// 情況2:買單價格低於最優賣單價格,無法成交
// 注意:使用compareTo方法比較BigDecimal,避免equals方法的精度問題
if (buyOrder.price.compareTo(bestSell.price) < 0) {
// 將買單加入買盤,等待後續匹配
buyBook.add(buyOrder);
break;
}
// 情況3:可以成交,計算可成交數量
// 取買單和賣單剩餘數量的最小值
long matchQty = Math.min(buyOrder.quantity, bestSell.quantity);
// 生成成交記錄,使用賣單價格作為成交價格
Trade trade = new Trade(buyOrder.sequenceId, bestSell.sequenceId, bestSell.price, matchQty);
System.out.println("✅ 成交: " + trade);
// 更新買單和賣單的剩餘數量
buyOrder.quantity -= matchQty;
bestSell.quantity -= matchQty;
// 如果賣單完全成交,從賣盤移除
if (bestSell.quantity == 0) {
sellBook.remove(bestSell);
}
// 繼續循環,嘗試匹配買單的剩餘數量
}
}
/**
* 撮合賣單邏輯
* 嘗試將賣單與買盤訂單進行匹配,遵循價格優先、時間優先原則
* @param sellOrder 待撮合的賣單
*/
private void matchSellOrder(OrderEntity sellOrder) {
// 循環撮合,直到賣單完全成交或無法匹配
while (sellOrder.quantity > 0) {
// 獲取買盤中的最優買單(價格最高的買單)
OrderEntity bestBuy = buyBook.getFirst();
// 情況1:買盤為空,沒有可匹配的買單
if (bestBuy == null) {
// 將賣單加入賣盤,等待後續匹配
sellBook.add(sellOrder);
break;
}
// 情況2:賣單價格高於最優買單價格,無法成交
// 注意:使用compareTo方法比較BigDecimal,避免equals方法的精度問題
if (sellOrder.price.compareTo(bestBuy.price) > 0) {
// 將賣單加入賣盤,等待後續匹配
sellBook.add(sellOrder);
break;
}
// 情況3:可以成交,計算可成交數量
// 取賣單和買單剩餘數量的最小值
long matchQty = Math.min(sellOrder.quantity, bestBuy.quantity);
// 生成成交記錄,使用買單價格作為成交價格
Trade trade = new Trade(bestBuy.sequenceId, sellOrder.sequenceId, bestBuy.price, matchQty);
System.out.println("✅ 成交: " + trade);
// 更新賣單和買單的剩餘數量
sellOrder.quantity -= matchQty;
bestBuy.quantity -= matchQty;
// 如果買單完全成交,從買盤移除
if (bestBuy.quantity == 0) {
buyBook.remove(bestBuy);
}
// 繼續循環,嘗試匹配賣單的剩餘數量
}
}
/**
* 主方法,用於測試撮合引擎功能
* @param args 命令行參數(未使用)
*/
public static void main(String[] args) {
// 創建撮合引擎實例
MatchEngine engine = new MatchEngine();
// 測試場景1:添加多個賣單,觀察賣盤排序
// 預期:賣盤按價格從低到高排序:100.40 -> 100.50 -> 100.60
engine.submitOrder(Direction.SELL, new BigDecimal("100.50"), 100);
engine.submitOrder(Direction.SELL, new BigDecimal("100.60"), 200);
engine.submitOrder(Direction.SELL, new BigDecimal("100.40"), 150);
// 測試場景2:添加買單,觸發成交
// 預期:
// 1. 與100.40的賣單成交150股
// 2. 與100.50的賣單成交50股
// 3. 買單完全成交,退出
engine.submitOrder(Direction.BUY, new BigDecimal("100.50"), 200);
// 測試場景3:添加高價買單,觸發更多成交
// 預期:
// 1. 與剩餘的100.50賣單成交50股
// 2. 與100.60的賣單成交200股
// 3. 買單剩餘50股,加入買盤
engine.submitOrder(Direction.BUY, new BigDecimal("100.70"), 300);
// 測試場景4:添加低價賣單,觸發成交
// 預期:
// 1. 與買盤中100.70的買單成交50股
// 2. 買單完全成交,退出
engine.submitOrder(Direction.SELL, new BigDecimal("100.30"), 50);
}
}