撮合的臨界區在於「從對手方拿出一筆最優訂單並移除」。若用兩步(先查再刪),在多pod或多執行緒情況下就會出現交易問題。我改用一支 Lua,在 Redis 內 一次把「查詢最優 → 立即移除」做成原子操作。
我把買單與賣單分別用不同的 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;
}
設計要點
我把關鍵臨界區定義為:從對手簿挑出「當前最優、可成交」的 maker,並在同一個不可分割的動作中把它自訂單簿移除(對其他流程隱形)。這一步用 Lua 在 Redis 內原子完成;其餘像成交量計算、事件組裝與剩餘回簿,則留在 Java 層處理。這種切分把臨界區縮到最小,只讓 Redis 單執行緒執行幾個 O(log N) 指令(ZRANGE/ZREVRANGE + ZREM),整體吞吐與可預測性更好。
好處: