在整理撮合細節之前,我先把「事件怎麼流動、各服務扮演什麼角色」說清楚。後續介紹任何一段程式碼時,都知道它站在整條鏈上的哪個位置。
Wallet → MatchEngine:錢包核定成功後發出 order.created,代表「這張單已經有相對應的資產鎖定,可以進入撮合」。
MatchEngine 撮合:收到 order.created 後,我在撮合服務內嘗試與對手方匹配;若有成交就產生成交事件;若吃不完就把剩餘量掛回訂單簿。
撮合輸出:我同時對外發出 order.matched(給order-service寫入 DB)以及 wallet.matched(給wallet-service做最終資產結算)。
查詢與撤單(補充):我另外提供查詢與撤單的 REST 介面,方便前端或測試工具直接操作。
我使用 Spring AMQP 在撮合引擎中監聽 order.created.queue。收到核定完成的事件後,整筆丟進撮合服務處理。
// OrderConfirmedListener.java
@RabbitListener(queues = "order.created.queue")
public void handleConfirmedOrder(OrderCreatedEvent event) throws JsonProcessingException {
matchingEngineService.tryMatch(event);
}
重點說明
撮合服務的主要邏輯做了三件事情:拿對手單 → 算成交量 → 發成交 / 處理剩餘。
// MatchingEngineService.java
public void tryMatch(OrderCreatedEvent incoming) {
boolean isBuy = incoming.getOrderType().equalsIgnoreCase("BUY");
while (incoming.getAmmount() > 0) {
OrderCreatedEvent maker = orderBookService.getAndRemoveBestMatchOrderLua(isBuy, incoming.getPrice());
if (maker == null) {
orderBookService.addOrder(incoming); // 吃不到對手 → 剩餘掛回簿
break;
}
int fill = Math.min(incoming.getAmmount(), maker.getAmmount());
incoming.setAmmount(incoming.getAmmount() - fill);
maker.setAmmount(maker.getAmmount() - fill);
OrderMatchedEvent matched = OrderMatchedEvent.builder()
.buyerId(isBuy ? incoming.getUserId() : maker.getUserId())
.sellerId(isBuy ? maker.getUserId() : incoming.getUserId())
.originBuyerPrice(isBuy ? incoming.getPrice() : maker.getPrice())
.originSellerPrice(isBuy ? maker.getPrice() : incoming.getPrice())
.dealPrice(maker.getPrice())
.amount(fill)
.matchedAt(LocalDateTime.now())
.orderType(incoming.getOrderType())
.build();
rabbitTemplate.convertAndSend(ORDER_EXCHANGE, ORDER_MATCHED_KEY, matched);
rabbitTemplate.convertAndSend(ORDER_EXCHANGE, WALLET_MATCHED_KEY, matched);
if (maker.getAmmount() > 0) {
orderBookService.addOrder(maker); // 對手單部分成交 → 剩餘存回redis中
} else {
orderBookService.removeOrder(maker);
// 對手單吃完 → 從redis移除
}
}
}
我在這裡的取捨
• 撮合價格:採用對手方現價(maker 價),符合撮合常見的價優先邏輯。
• 兩條事件:order.matched 與 wallet.matched 分流,讓訂單與錢包結算分開處理。
• 剩餘處理:任何一方的剩餘都以「更新後的訂單」回簿,保證訂單簿是最新狀態。
我把 Exchange 與 Routing Key 集中到共用常數,避免 typo、也好做查找替換。
// RabbitMQConstants.java(節選)
public static final String ORDER_EXCHANGE = "order.exchange";
public static final String ORDER_MATCHED_KEY = "order.matched";
public static final String WALLET_MATCHED_KEY = "wallet.matched";
今天先簡單講撮和流程的主要過程,明天會細說如何進行Redis上的訂單簿設計,以及如何進行保證原子性的交易