iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Software Development

事件驅動電力交易平台:Spring Boot 實戰系列 第 12

Day 12|撮合事件流全覽:從核定回報 → 撮合 → 成交回寫

  • 分享至 

  • xImage
  •  

1. 為什麼要先看全貌

在整理撮合細節之前,我先把「事件怎麼流動、各服務扮演什麼角色」說清楚。後續介紹任何一段程式碼時,都知道它站在整條鏈上的哪個位置。

2. 事件流概覽

  1. Wallet → MatchEngine:錢包核定成功後發出 order.created,代表「這張單已經有相對應的資產鎖定,可以進入撮合」。

  2. MatchEngine 撮合:收到 order.created 後,我在撮合服務內嘗試與對手方匹配;若有成交就產生成交事件;若吃不完就把剩餘量掛回訂單簿。

  3. 撮合輸出:我同時對外發出 order.matched(給order-service寫入 DB)以及 wallet.matched(給wallet-service做最終資產結算)。

  4. 查詢與撤單(補充):我另外提供查詢與撤單的 REST 介面,方便前端或測試工具直接操作。

3. 事件入口(Listener)

我使用 Spring AMQP 在撮合引擎中監聽 order.created.queue。收到核定完成的事件後,整筆丟進撮合服務處理。

// OrderConfirmedListener.java
@RabbitListener(queues = "order.created.queue")
public void handleConfirmedOrder(OrderCreatedEvent event) throws JsonProcessingException {
  matchingEngineService.tryMatch(event);
}

重點說明

  • 我把「監聽」與「撮合」責任分離,Listener 只做轉交。
  • 事件的語意是「可被撮合」,因此後續流程不需要再詢問錢包。

4. 撮合主流程(Service)

撮合服務的主要邏輯做了三件事情:拿對手單 → 算成交量 → 發成交 / 處理剩餘。

// 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 分流,讓訂單與錢包結算分開處理。
• 剩餘處理:任何一方的剩餘都以「更新後的訂單」回簿,保證訂單簿是最新狀態。

5. 事件常數集中管理

我把 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";

6. 小結

今天先簡單講撮和流程的主要過程,明天會細說如何進行Redis上的訂單簿設計,以及如何進行保證原子性的交易


上一篇
Day 11|查得見的進度:Order Service 的訂單狀態追蹤
系列文
事件驅動電力交易平台:Spring Boot 實戰12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言