iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Software Development

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

Day 14|訂單媒合Lua 原子操作:對手方最優價 → 取單 → 移除(一次完成

  • 分享至 

  • xImage
  •  

1. 為什麼一定要 Lua

撮合的臨界區在於「從對手方拿出一筆最優訂單並移除」。若用兩步(先查再刪),在多pod或多執行緒情況下就會出現交易問題。我改用一支 Lua,在 Redis 內 一次把「查詢最優 → 立即移除」做成原子操作。

2. 單筆原子取得與移除(核心腳本)

我把買單與賣單分別用不同的 ZRANGE* 指令取最優價方向,並在同一隻腳本裡 ZREM 掉取出的成員,確保沒有「讀完還沒刪就被別人拿走」的可能性。

// RedisOrderBookService.getAndRemoveBestMatchOrderLua(...)
public OrderCreatedEvent getAndRemoveBestMatchOrderLua(boolean isBuy, int price) {
  String zsetKey = isBuy ? SELL_ORDERBOOK_KEY : BUY_ORDERBOOK_KEY;

  final String buyLua =
    "local r = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1); " +
    "if #r > 0 then redis.call('ZREM', KEYS[1], r[1]); return r[1]; else return nil; end";

  final String sellLua =
    "local r = redis.call('ZREVRANGEBYSCORE', KEYS[1], '+inf', ARGV[1], 'LIMIT', 0, 1); " +
    "if #r > 0 then redis.call('ZREM', KEYS[1], r[1]); return r[1]; else return nil; end";

  String orderId = (String) redisTemplate.execute((RedisCallback<String>) conn -> {
    Object res = conn.eval((isBuy ? buyLua : sellLua).getBytes(), ReturnType.VALUE, 1,
        zsetKey.getBytes(), Integer.toString(price).getBytes());
    return res != null ? new String((byte[]) res) : null;
  });

  if (orderId == null) return null;
  String json = redisTemplate.opsForValue().get("order:" + orderId);
  return json != null ? objectMapper.readValue(json, OrderCreatedEvent.class) : null;
}

設計要點

  • 買單吃賣方:ZRANGEBYSCORE ... '-inf' → price 取最低價的一筆。
  • 賣單吃買方:ZREVRANGEBYSCORE ... '+inf' → price 取最高價的一筆。
  • 取回的只有 orderId,我再去 order:{id} 撈 JSON 還原成事件物件。

3. 為什麼把「取單」做成 Lua 臨界區?

我把關鍵臨界區定義為:從對手簿挑出「當前最優、可成交」的 maker,並在同一個不可分割的動作中把它自訂單簿移除(對其他流程隱形)。這一步用 Lua 在 Redis 內原子完成;其餘像成交量計算、事件組裝與剩餘回簿,則留在 Java 層處理。這種切分把臨界區縮到最小,只讓 Redis 單執行緒執行幾個 O(log N) 指令(ZRANGE/ZREVRANGE + ZREM),整體吞吐與可預測性更好。
好處:

  • 正確性:同一張訂單只會被一個流程拿到,杜絕重複撮合與負數剩餘。
  • 高吞吐:臨界區短小、只含 O(log N) 操作,降低對 Redis 的阻塞時間。
  • 易擴充:業務後處理在 Java 層,計算、發事件、回簿互不牽扯,變更成本低。
  • 少依賴:不需要分散式鎖(如 Redlock),部署與排障更單純。
  • 可恢復:若在回簿前發生故障,影響範圍可透過事件重放/對帳機制處理。

上一篇
Day 13|訂單簿模型:Redis 上的 Buy/Sell、訂單詳情、使用者索引
下一篇
Day 15|部分/全部成交與撤單:回簿、移除與 REST 控制
系列文
事件驅動電力交易平台:Spring Boot 實戰16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言