脈絡即使寄件端只送了一次訊息,接收端仍可能收到不只一次。

重複訊息從哪裡來#

  • 可靠訊息實作本身——通道模式談過用 Guaranteed Delivery 讓通道可靠,但有些可靠訊息實作會產生重複訊息
  • 底層協定本來就不可靠——在許多 B2B 整合情境中,訊息得透過網際網路以 HTTP 傳送,此時只能靠「重送直到收到確認」來保證送達
  • 刻意允許重複以換取吞吐量——許多訊息系統內建消除重複訊息的機制,讓應用程式不必操心;但在基礎設施中消除重複會帶來額外開銷
  • 失敗的分散式交易——許多透過商業配接器接上訊息基礎設施的套裝應用無法正確參與分散式兩階段提交

當一則訊息被送給多個應用程式、而其中一或多個因此失敗時,要從這種不一致狀態復原可能很困難

解法#

達成冪等性有兩種主要途徑:

  1. 明確去重(de-duping)——移除重複訊息
  2. 定義訊息語意使其本身即具冪等性

途徑一:明確去重#

接收端可以追蹤自己已經收過哪些訊息來明確去重。

許多訊息系統(例如相容 JMS 的工具)會自動為每則訊息指派唯一識別碼,應用程式完全不必操心。

該保存多久的歷史?#

要依識別碼偵測並剔除重複,接收端必須保存一份「已收識別碼」的緩衝。

  • 最簡單的情況:寄件端一次只送一則訊息,每則都等接收端確認。此時接收端只需把每則進站訊息的識別碼與前一則比較,相同就忽略——實際上只保存一則訊息的歷史。
  • 較有效率的情況:寄件端不等每則確認就送出一整批訊息。但這意味著接收端得保存較長的識別碼歷史——「記憶」的大小取決於寄件端在沒收到確認的情況下能送出多少則訊息

這個問題與 Resequencer 中的考量非常相似。

消除重複訊息又是一個能從低階 TCP/IP 協定學到很多的例子:IP 封包跨網路路由時可能產生重複封包,TCP/IP 靠為每個封包附上唯一識別碼來消除重複;收發雙方協商一個「視窗大小」,由接收端配置以偵測重複。

陷阱:別拿業務鍵當訊息識別碼#

有時候很誘人地會想用業務鍵當訊息識別碼,讓持久化層負責去重

例如應用程式把進站訂單存進資料庫,若每張訂單有唯一訂單編號、且我們在該欄位上設唯一鍵,那麼收到重複訂單訊息時,插入操作就會失敗

這看起來很優雅——我們把重複檢查委派給了極擅長偵測重複鍵的資料庫。

想像業務需求改變,客戶可以用「同一個訂單編號的另一則訊息」來修改既有訂單(這相當常見)。因為我們把唯一訊息識別碼綁在業務欄位上,現在就得修改訊息結構了。

因此最好避免讓單一欄位承載雙重語意。

不過,用資料庫強制去重有時確實會被用在訊息基礎設施廠商提供的資料庫配接器上——因為許多這類配接器沒有消除重複的能力,只好把這個功能委派給資料庫。

途徑二:讓訊息語意本身冪等#

另一種達成冪等的方式,是把訊息的語意定義成「重送不影響系統」

例如與其定義成「把帳戶 12345 加 10 美元」,改成「把帳戶 12345 的餘額設為 110 美元」。若當前餘額是 100 美元,兩則訊息達成相同結果,但第二則是冪等的——收到兩次也不會有任何影響。

這個例子忽略了並行狀況——例如在原始訊息與重複訊息之間,又來了一則「把帳戶 12345 的餘額設為 150 美元」。

範例:Microsoft IDL(MIDL)

Microsoft Interface Definition Language(MIDL)把冪等性納為遠端呼叫語意的一部分,可用 [idempotent] 屬性宣告某個遠端程序是冪等的。

MIDL 規格說明:「[idempotent] 屬性指出某個操作不修改狀態資訊,且每次執行都回傳相同結果。執行該常式多次的效果與執行一次相同。」

interface IFoo;
[
    uuid(5767B67C-3F02-40ba-8B85-D8516F20A83B),
    pointer_default(unique)
]

interface IFoo
{
    [idempotent]
    bool GetCustomerName
    (
         [in] int CustomerID,
         [out] char *Name
    );
}