← 主題

跨產品餘額同步:用 WebSocket 即時顯示另一個產品的餘額

產品 A 把使用者餘額轉入產品 B、A 的客戶端又要顯示 B 的餘額時,經由 A 的後端輪詢會讓 A 的處理能力受制於 B 的延遲。附流程圖說明 B 透過 WebSocket 推送每次變動如何解決。

兩個產品之間常見的做法:使用者把一部分餘額從產品 A 轉入產品 B,再到 B 裡使用。使用者在 B 裡花掉或贏得金額時,A 的客戶端仍然要即時顯示 B 裡還剩多少。第一版通常用 HTTP 去問:A 的客戶端輪詢 A 的後端,A 的後端再去問 B。每一次輪詢都變成橫跨兩個產品的請求鏈,流量一上來,限制你能服務多少使用者的就不是任何一個產品,而是這條鏈。

請求鏈是怎麼疊起來的

轉入餘額

A 的客戶端
POST /transfer
A 的後端等待 B
POST /deposit
B 的後端

在 A 的客戶端顯示 B 的餘額

A 的客戶端每 5 秒
GET /b-balance
A 的後端等待 B
GET /balance
B 的後端每個請求佔一個 worker
A 的客戶端每 5 秒
GET /b-balance
A 的後端等待 B
GET /balance
B 的後端每個請求佔一個 worker
改之前:每個使用者每隔幾秒,A 的客戶端問 A 的後端,A 的後端再問 B

每個 HTTP 請求在等待時都會佔住資源:一個 worker 執行緒、用戶端連線池裡的一條連線,通常還有一條資料庫連線。A 的請求要等 B 回應後才能回應,所以佔住這些資源的時間是自己的處理時間,加上 B 花掉的全部時間。Little 定律可以算出這個數字:處理中的請求數 = 每秒請求數 × 每個請求花的秒數。而且只要有 A 的使用者開著頁面,B 每隔幾秒就會收到一個請求,不管餘額有沒有變。

要付出多少 worker

以下是推算範例,不是壓測結果(撰寫於 2026-11-09):A 有 5,000 名使用者的畫面上顯示著 B 的餘額,A 的客戶端每 5 秒輪詢一次,所以每秒有 1,000 個請求經過 A 送到 B。

痛點在最後一列。假設連線池有 200 個 worker,B 一時變慢就足以把 A 的用光:A 自己的請求,包括登入和付款,都在佇列裡排隊、逾時,用戶端再重試,又加進更多請求。A 的併發量正好在流量高峰時下滑,原因卻是一個 A 根本不經營的產品。同時,B 也把自己的處理能力花在回應大多是「沒變」的輪詢上。

改之後:B 每次變動只發佈一次

A 的客戶端
POST /transfer
A 的後端
POST /deposit
B 的後端寫入 B 的帳本
B 的後端每次變動後
發佈 balance.updated
Hibiraprivate-b-balance-42
WebSocket
A 的客戶端以及 B 自己的客戶端
改之後:B 發佈使用者餘額的每次變動,A 的客戶端直接收到
A 的客戶端A 的後端B 的後端Hibira轉 1,000 到 BPOST /deposit冪等鍵200B 餘額 1,000,版本 1balance.updatedprivate-b-balance-42balance.updated1,000,版本 1之後使用者在 B 花了 120balance.updated880,版本 2balance.updated880,版本 2
使用者轉 1,000 到 B,之後在 B 花了 120;A 的客戶端兩次都立刻看到

B 每寫入一次變動(包括轉入)就發佈一次。A 的客戶端保持一條 WebSocket 連線,一趟網路往返就收到新餘額。B 的餘額不再經過 A 的後端,所以變慢或忙碌的 B 佔不住 A 的 worker,B 也不用再回應成千上萬的輪詢。

怎麼設定

  • B 的餘額由 B 負責。 B 的帳本是 B 裡剩多少的唯一事實來源。每次提交後,B 用任何 Pusher 伺服器端函式庫的 HTTP API 發佈 balance.updated,帶上新餘額和版本號。
  • 每個使用者一個私有頻道。 例如 private-b-balance-42。如果 A 和 B 用同一個應用,A 的授權端點確認登入的使用者是 42 之後簽署訂閱。如果 B 有自己的應用,A 的客戶端就用 B 的應用金鑰連線,由 B 的授權端點簽署。如果中繼服務只該看到密文,就用 private-encrypted-b-balance-42;其他私有頻道在傳輸中由 TLS 保護,儲存時加密。
  • 善用版本號。 版本比畫面上低的事件直接忽略。A 的客戶端連線或重新連線時,先用 HTTP 讀一次 B 的餘額,再套用之後的事件。
  • A 的後端也能收。 如果 A 在伺服器上需要 B 的餘額,例如讓使用者把錢轉回來之前,可以用 Pusher 用戶端函式庫靠同樣的事件維護一份副本,不必每次都呼叫 B。

不變的部分

從 A 轉到 B 仍然是帶冪等鍵的 HTTP 請求,重試也絕不會把錢轉兩次;B 每次寫入時也仍會自己檢查餘額。WebSocket 傳送的是「B 的餘額變了」這個消息,不是變動本身。如果另一端的產品是外部 API 供應商,轉帳本身也可能塞爆佇列:請看與 API 供應商的錢包轉帳。

常見問題

為什麼不乾脆降低輪詢頻率?

間隔拉長,請求變少,但餘額也變舊:在 B 贏得的金額要晚一整個間隔才出現在 A 的客戶端,而且每次輪詢仍然要等 B。推送一次變動只要一趟網路往返,沒有變動時什麼都不送。

A 的客戶端斷線時漏掉事件怎麼辦?

pusher-js 和其他用戶端函式庫會自動重新連線。重新連線後先用 HTTP 讀一次 B 的餘額,只套用版本更高的事件。版本號也讓重複或遲到的事件無害。

A 需要改 B 的程式嗎?

B 只要加一件事:寫入餘額後發佈 balance.updated,如果 B 有自己的應用,再加上簽署訂閱。它的轉帳 API 維持不變。

這些事件在 Hibira 上怎麼計算?

每次發佈算一則訊息。付費方案不計算送給訂閱者的份數,所以同一個餘額同時顯示在 A 和 B 的客戶端,費用和只顯示在一個客戶端相同。請看價格。

產品名稱為其所有者的商標。