脈絡:我的應用程式正用訊息傳遞執行 Request-Reply,而且收到了一則回覆訊息。
非同步帶來的混亂#
一個行程透過遠端程序呼叫喚起另一個時,呼叫是同步的,因此不會搞混哪次呼叫產生了哪個結果。但訊息傳遞是非同步的:從呼叫端的角度看,它發出了呼叫,然後過了一陣子有個結果冒出來。
呼叫端可能根本不記得自己發過這個請求,或發過太多請求以致不知道這是哪一個的結果。混亂到這個地步,等結果終於來了它也不知道能拿它怎麼辦——那當初發出這個呼叫的目的就落空了。
幾個行不通的替代方案:
- 一次只發一個呼叫,收到回覆才發下一個——吞吐量會大幅下降。
- 假設回覆的順序與請求的順序相同——但訊息傳遞不保證遞送順序,而且不同請求的處理時間也不一樣,這個假設是錯的。
- 把請求設計成不需要回覆——這個限制會讓訊息傳遞在許多用途上變得無用。
呼叫端需要的,是讓回覆訊息帶有指向請求訊息的指標或參考。但訊息並不存在於可被變數指涉的穩定記憶體空間中——不過,訊息可以帶一個外部鍵,像關聯式資料庫表格的主鍵那樣的唯一識別碼,用來把這則訊息與其他訊息、使用它的客戶端等等區分開來。
解法#
Correlation Identifier 有六個組成部分:
- Requestor(請求方)——透過送出請求並等待回覆來完成某項業務任務的應用程式
- Replier(回覆方)——接收請求、完成它、再送出回覆的應用程式。它從請求中取出 request ID,並把它當作 correlation ID 存進回覆
- Request(請求)——從請求方送到回覆方、含有 request ID 的訊息
- Reply(回覆)——從回覆方送到請求方、含有 correlation ID 的訊息
- Request ID——請求中用來唯一識別該請求的一個 token
- Correlation ID——回覆中的一個 token,其值與請求中的 request ID 相同
運作流程:
- 請求方建立請求訊息時,指派一個 request ID——一個與所有當前未完成請求都不同的識別碼
- 回覆方處理請求時,保存這個 request ID,並把它當作 correlation ID 加進回覆
- 請求方處理回覆時,用 correlation ID 得知這是哪一個請求的回覆
之所以叫「關聯」識別碼,是因為呼叫端用這個識別碼把每則回覆與引發它的請求**關聯(比對、建立關係)**起來。
雙方必須先談好#
一如訊息傳遞的常態,請求方與回覆方必須就多項細節達成共識:
- request ID 屬性的名稱與型別
- correlation ID 屬性的名稱與型別
- 請求與回覆的訊息格式必須定義這些屬性,或允許把它們當作自訂屬性加進去
例如請求方把 request ID 存成一個名為
request_id的第一層 XML 元素、值為整數,回覆方必須知道這件事才找得到並正確處理它。request ID 與 correlation ID 通常是同型別;若不是,請求方就得知道回覆方會怎麼把 request ID 轉成 reply ID。
該放在表頭#
correlation ID(以及 request ID)通常放在訊息的表頭而非內文。這個 ID 不是請求方想傳達給回覆方的命令或資料的一部分——事實上回覆方根本沒真的用到它,它只是從請求中把 ID 存下來、再加進回覆,一切都是為了請求方的方便。既然訊息內文是兩個系統之間傳輸的內容,而 ID 不屬於那份內容,它就該放進表頭。
三種實作作法#
模式的要旨是:回覆訊息帶著一個 token(correlation ID),用來識別對應的請求(透過它的 request ID)。達成方式有幾種:
用訊息 ID#
最簡單的作法:每個請求帶一個唯一 ID(例如訊息 ID),回應的 correlation ID 就是該請求的唯一 ID。
但請求方在處理回覆時,知道「是哪則請求訊息」往往沒什麼意思。它真正想被提醒的是:當初是哪一項業務任務促使它送出這個請求,好用回覆中的資料把那項業務任務完成。
用業務物件 ID#
業務任務(執行一筆股票交易、出貨一張採購單)多半有自己的唯一業務物件識別碼(例如訂單 ID),那就可以拿它當作請求-回覆的 correlation ID。
這樣請求方拿到回覆與其 correlation ID 時,就能繞過請求訊息、直接找到當初引發這個請求的那個業務物件。
此時收發雙方不該用訊息內建的 request message ID 與 reply correlation ID 屬性,而該在請求與回覆中使用一個自訂的業務物件 ID 屬性。
折衷:請求方自己維護對照表#
請求方保存一份 request ID 與業務物件 ID 的對照表。
這在兩種情況下特別有用:請求方想把業務物件 ID 保密;或請求方管不到回覆方的實作,只能仰賴對方把請求的 message ID 複製到回覆的 correlation ID。
收到回覆時,請求方在對照表中查出 correlation ID 對應的業務物件 ID,再用它接續那項業務任務。
串接請求-回覆#
訊息之所以同時有 message ID 與 correlation ID 兩個屬性,是為了讓請求-回覆訊息對能串接起來:一個請求引出一個回覆,而這個回覆本身又是另一個請求、再引出另一個回覆,如此下去。
- 訊息的 message ID 唯一識別它所代表的請求
- 若訊息同時帶有 correlation ID,那它也是某則請求訊息的回覆——由該 correlation ID 指明
只有當應用程式想從最新的回覆一路回溯到最初的請求時,串接才有用。多數時候應用程式只想知道最初的那個請求,中間經過幾層回覆並不重要。
在這種情況下:訊息一旦帶有非空的 correlation ID,它就是一則回覆,而所有由它引發的後續回覆都應沿用同一個 correlation ID。
相關模式#
本模式是 Asynchronous Completion Token 模式 [POSA2] 在訊息傳遞領域的簡化版:請求方是 Initiator、回覆方是 Service、請求方中處理回覆的消費者是 Completion Handler,而那個用來比對回覆與請求的 Correlation Identifier 就是 Asynchronous Completion Token(只是這裡的 token 就是個原始值)。
- Correlation Identifier 用來把回覆與它的請求配對;請求可能同時帶有 Return Address 說明該把回覆放到哪個通道。
- Message Sequence 的識別碼則是用來指出某則訊息在同一寄件端所送出的一系列訊息中的位置。
範例:JMS、.NET 與 Web Services
JMS 的 Correlation-ID 屬性#
JMS 訊息有預先定義好的 JMSCorrelationID 屬性,通常與另一個預定義屬性 JMSMessageID 搭配使用。回覆訊息的 correlation ID 由請求的 message ID 設定:
Message requestMessage = // Get the request message
Message replyMessage = // Create the reply message
String requestID = requestMessage.getJMSMessageID();
replyMessage.setJMSCorrelationID(requestID);.NET 的 Correlation-Id 屬性#
.NET 中每則 Message 都有 CorrelationId 屬性——確認訊息中的一個字串,通常設為原始訊息的 Id。MessageQueue 另有特殊的 PeekByCorrelationId(string) 與 ReceiveByCorrelationId(string) 方法,可用來窺看或消費佇列上帶有指定 correlation ID 的訊息(見 Selective Consumer)。
Web Services 的請求/回應#
到 SOAP 1.1 為止,Web services 標準對非同步訊息的支援都不太好,SOAP 1.2 才開始為它做準備——它納入了 Request-Response Message Exchange Pattern,這是非同步 SOAP 訊息的基礎。
但這個請求/回應模式並未強制支援「多個進行中的請求」,因此它沒有定義標準的 Correlation Identifier 欄位,連可選的都沒有。
實務上服務請求方常常確實需要多個未完成的請求。〈Web Services Architecture Usage Scenarios〉[WSAUS] 討論了數種非同步 Web service 情境,其中四種——Request/Response、Remote Procedure Call(傳輸協定不直接支援同步請求/回應時)、Multiple Asynchronous Responses 與 Asynchronous Messaging——都在 SOAP 表頭中用 message-id 與 response-to 欄位把回應與請求關聯起來。
含有訊息識別碼的 SOAP 請求:
<?xml version="1.0" ?>
<env:Envelope xmlns:env="http://www.w3.org/2002/06/soap-envelope">
<env:Header>
<n:MsgHeader xmlns:n="http://example.org/requestresponse">
<n:MessageId>uuid:09233523-345b-4351-b623-5dsf35sgs5d6</n:MessageId>
</n:MsgHeader>
</env:Header>
<env:Body>
........
</env:Body>
</env:Envelope>含有與原始請求之關聯的 SOAP 回應:
<?xml version="1.0" ?>
<env:Envelope xmlns:env="http://www.w3.org/2002/06/soap-envelope">
<env:Header>
<n:MsgHeader xmlns:n="http://example.org/requestresponse">
<n:MessageId>uuid:09233523-567b-2891-b623-9dke28yod7m9</n:MessageId>
<n:ResponseTo>uuid:09233523-345b-4351-b623-5dsf35sgs5d6</n:ResponseTo>
</n:MsgHeader>
</env:Header>
<env:Body>
........
</env:Body>
</env:Envelope>和 JMS 與 .NET 的例子一樣:請求訊息含有唯一的訊息識別碼,回應訊息含有一個 ResponseTo(也就是 correlation ID)欄位,其值即為請求訊息的識別碼。