
写这篇文章的出发点很简单:把"撮合系统(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版本)。
并发与一致性:如何在保证撮合顺序的同时实现高并发接入,是系统设计的核心挑战之一。
持久化与恢复:崩溃恢复需要能从交易日志重放到某一时刻,确保账户与订单状态一致。
风控与防攻击:防止刷单、闪电撤单、异常流量;撮合前的余额冻结、下单速率限制、黑名单等都很重要。
测试覆盖:单元测试、压力测试、回放测试(用历史订单回放检查一致性)、混沌测试(故障注入)不可或缺。
如果只是起步:先用单线程、明确的撮合规则、完善的测试和日志,再在瓶颈处做优化。先正确后做快。
关注可恢复性:总是把写前日志(WAL)和幂等回放做好,系统一旦崩溃能重建状态是底线。
设计隔离:把撮合、账务、风控、广播模块拆开,按接口通信,这样更易演进和扩展。
多做场景测试:市场突然断货、大量市价单涌入、网络抖动、机器重启……这些都是线上常见灾难场景。
撮合系统看起来是"撮合买卖",但工程实现里涉及到并发、延迟、持久化、风控和大量测试。把基本规则(价格优先、时间优先)弄清楚,再逐步把性能和可靠性打磨好,是把一台可上线的撮合引擎交付的正确路径。如果你想把某一部分讲得更细(比如撮合器的线程模型、订单簿数据结构比较、或如何做高并发下的撤单优化),告诉我你关心的点,我可以把那一块拆成专门的技巧篇。
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);
}
}原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。