這個服務設計上原本就假設會有多個實例同時運作,MySQL 與 Redis 由所有實例共用。假設現在有機器A、機器B,Alice 的 WebSocket 連在機器A,訂單到期後,各台排程會從Redis搶著認領;她參與的那張訂單,這次由機器B搶到並完成扣款,結算完成後,機器B要通知 Alice。
伺服器以 convertAndSendToUser 指定收件人,再由WebSocket的broker找到這位使用者的連線並送達。
假設這次由機器 B 完成扣款,接著呼叫 convertAndSendToUser("alice", ...) 發送通知,convertAndSendToUser() 只能從目前機器B的 WebSocket 連線中尋找 Alice,可是Alice的連線在機器 A,機器 B 並不知道這條連線存在,因此通知無法送出。

在這個通知送出失敗的情境下,程式不會拋出例外、也不寫error log,WebSocket 的連線與訂閱狀態都只存在建立連線的那台 JVM(機器A),其他機器看不到。
機器 B 完成扣款後,不直接把通知送給 WebSocket,而是先呼叫 publish(),把「通知 alice」這件事發布到 Redis:
public void publish(String userId, String destination, Object payload) {
NotificationEnvelope envelope = new NotificationEnvelope(userId, destination, payload);
try {
String json = objectMapper.writeValueAsString(envelope);
redisTemplate.convertAndSend(CHANNEL, json); // websocket:notifications
} catch (JsonProcessingException e) {
log.error("Failed to serialize notification envelope for user={}, destination={}", ...);
}
}
這裡的 userId 是收件人;destination 是 WebSocket 要送到的目的地;payload 則是通知內容,這些資料會被包進 NotificationEnvelope,再透過 CHANNEL 發布到 Redis。
下一步Redis Pub/Sub 會接著把這則訊息送給所有訂閱這個 CHANNEL 的機器,所以機器 A、B 都會收到同一個 NotificationEnvelope。
每台機器收到 Redis 訊息後,再交給本機的 WebSocket broker:
@Override
public void onMessage(Message message, byte[] pattern) {
try {
NotificationEnvelope envelope =
objectMapper.readValue(new String(message.getBody()), NotificationEnvelope.class);
messagingTemplate.convertAndSendToUser(
envelope.getUserId(), envelope.getDestination(), envelope.getPayload());
} catch (Exception e) {
log.warn("Failed to relay notification from Redis: {}", e.getMessage());
}
}
這裡才會真正把 Redis 收到的訊息交給本機的 WebSocket broker,機器A 有 Alice 的連線,所以 convertAndSendToUser() 可以送達;機器B沒有Alice的連線,就不會送出任何東西。讓每台機器都收到通知,再由各自的 WebSocket broker 判斷這個使用者是否連在自己這台機器上。
假使使用者當下離線或正在重連,這則通知就會直接消失,沒有補送機制。因此,目前結算通知功能只能做到盡力而為,不保證送達。