iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

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

Day 13|訂單簿模型:Redis 上的 Buy/Sell、訂單詳情、使用者索引

  • 分享至 

  • xImage
  •  

1. 設計目標

撮合效能來自「讀最優價與寫回」的快與穩。我用 Redis 建立簡單、直覺的三層模型:價位集合、訂單詳情、用戶id索引。這個設計支援高速的搓合以及快速查詢用戶訂單,也便於之後替換或升級。

2. 三個維度的 Key 設計

價位集合(ZSET)

Key = orderbook:buy(score=price)
Key = orderbook:sell(score=price)
value= orderId
我用分數做為價格排序,之後由業務層依照買賣單需求取最低或最高。

訂單詳情(Value / JSON)
Key = {orderId},value= OrderCreatedEvent 的 JSON
使用者索引(SET)
Key={userId},value=orders → 該使用者所有 orderId(查詢時先拿 id 再批次讀詳情,提高查詢效率)

3. 進簿:核定後掛到訂單簿

這裡的關鍵是「三個維度一起更新」:價位集合、詳情、使用者索引。

// RedisOrderBookService.addOrder(...)
public void addOrder(OrderCreatedEvent e) throws JsonProcessingException {
  String key = e.getOrderType().equalsIgnoreCase("BUY") ? BUY_ORDERBOOK_KEY : 
  SELL_ORDERBOOK_KEY;

  redisTemplate.opsForZSet().add(key, e.getOrderId().toString(), e.getPrice());
  redisTemplate.opsForValue().set("order:" + e.getOrderId(),  
  objectMapper.writeValueAsString(e));
  redisTemplate.opsForSet().add("user:" + e.getUserId() + ":orders", 
  e.getOrderId().toString());
}

設計理由

  • 我選擇「以 orderId 為成員」而不是把整包 JSON 塞進 ZSET,讓「讀詳情」與「排序」分開,各司其職。
  • 使用者索引讓查詢使用者的所有掛單變得容易,也能對接後續的 UI。

4. 出簿:吃完或撤單的移除

移除也要同步清三個維度,確保不遺留殘骸。

// RedisOrderBookService.removeOrder(...)
public void removeOrder(OrderCreatedEvent e) {
  String key = e.getOrderType().equalsIgnoreCase("BUY") ? BUY_ORDERBOOK_KEY : 
  SELL_ORDERBOOK_KEY;

  redisTemplate.opsForZSet().remove(key, e.getOrderId().toString());
  redisTemplate.delete("order:" + e.getOrderId());
  redisTemplate.opsForSet().remove("user:" + e.getUserId() + ":orders", 
  e.getOrderId().toString());
}

5. 查詢:先拿使用者索引,再撈詳情

我提供以人為中心的查詢,這在管理個人掛單列表時很好用。

// RedisOrderBookService.getOrderByUserId(...)
public List<OrderCreatedEvent> getOrderByUserId(UUID userId) {
  Set<String> ids = redisTemplate.opsForSet().members("user:" + userId + ":orders");
  if (ids == null || ids.isEmpty()) return List.of();

  return ids.stream()
    .map(id -> redisTemplate.opsForValue().get("order:" + id))
    .filter(Objects::nonNull)
    .map(json -> objectMapper.readValue(json, OrderCreatedEvent.class))
    .collect(Collectors.toList());
}

我在這裡刻意保留彈性

  • 若未來要嚴格時間優先(同價 FIFO),我可以把「時間資訊」編入 score 或改為「價位集合 + 價位佇列」雙層結構。

上一篇
Day 12|撮合事件流全覽:從核定回報 → 撮合 → 成交回寫
下一篇
Day 14|訂單媒合Lua 原子操作:對手方最優價 → 取單 → 移除(一次完成
系列文
事件驅動電力交易平台:Spring Boot 實戰16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言