前文學習瞭如何使用工作佇列在多個工作者之間分配耗時的任務。若需要在遠端計算機上執行一個函式並等待結果呢?這種模式通常被稱為遠端過程呼叫 (RPC)。
本節使用 RabbitMQ 構建一個 RPC 系統:一個客戶端和一個可擴充套件的 RPC 伺服器。由於我們沒有耗時的任務可以分配,因此我們將建立一個返回斐波那契數的虛擬 RPC 服務。
客戶端介面
建立一個簡單的客戶端類,暴露 call
方法,該方法傳送一個 RPC 請求並阻塞,直到收到響應:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println("fib(4) 是 " + result);
雖然 RPC 是計算中很常見的模式,但它經常受到批評。問題在於當程式設計師不確定函式呼叫是本地呼叫還是緩慢的 RPC 呼叫時,會引發困惑。這種混淆會導致系統不可預測,並增加除錯的複雜性。錯誤使用 RPC 不僅沒有簡化軟體,反而可能導致難以維護的“程式碼結構混亂”。
鑑於此,請遵循以下建議:
確保明確區分本地函式呼叫和遠端函式呼叫。
記錄你的系統,使元件之間的依賴關係清晰。
處理錯誤情況。例如,當 RPC 伺服器長時間不可用時,客戶端應如何響應?
如有疑慮,請儘量避免使用 RPC。如果可能,應該使用非同步管道——與 RPC 類似的阻塞操作不同,結果將被非同步推送到下一個計算階段。
回撥佇列
在 RabbitMQ 上實現 RPC 很簡單。客戶端傳送一個請求訊息,伺服器透過響應訊息進行回覆。為接收響應,需要在請求中附上一個“回撥”佇列地址。可用預設的佇列(在 Java 客戶端中是獨佔的)。試試這個程式碼:
callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties.Builder()
.replyTo(callbackQueueName)
.build();
channel.basicPublish("", "rpc_queue", props, message.getBytes());
// ...然後從 callback_queue 讀取響應訊息...
需要匯入:
import com.rabbitmq.client.AMQP.BasicProperties;
訊息屬性
AMQP 0-9-1 協議預定義了一組 14 個與訊息一起傳送的屬性。大多數屬性很少使用,以下屬性是常用的:
deliveryMode
:標記訊息為持久 (值為2
) 或瞬時 (其他值) 的模式
contentType
:用於描述編碼的 mime 型別。例如,對於常用的 JSON 編碼,建議將此屬性設定為:application/json
replyTo
:通常用於命名回撥佇列
correlationId
:用於將 RPC 響應與請求相關聯
Correlation Id
在前面提到的方法中,我們建議為每個 RPC 請求建立一個回撥佇列。這很低效,但幸好有一個更好的方法——為每個客戶端建立一個回撥佇列。
這會引發一個新問題:在回撥佇列中收到響應時,不清楚該響應屬於哪個請求。這時 correlationId
屬性派上用場。為每個請求設定一個唯一值。稍後,回撥佇列中收到訊息時,看此屬性,並根據它來匹配響應和請求。如看到一個未知 correlationId
值,可以安全地丟棄訊息——它不屬於我們的請求。
為啥應該忽略回撥佇列中的未知訊息,而不非直接失敗?因為伺服器端可能會發生競態條件。雖然不太可能,但可能 RPC 伺服器在傳送完答案後崩潰,但在為請求傳送確認訊息之前就崩潰了。如果發生這種情況,重啟後的 RPC 伺服器將重新處理該請求。因此,客戶端的我們必須優雅地處理重複的響應,RPC 最好是冪等的。
總結
RPC模式工作流程:
- 對於一個 RPC 請求,客戶端傳送一條帶有兩個屬性的訊息:
replyTo
,其值設定為為該請求建立的匿名獨佔佇列;correlationId
,其值為每個請求設定的唯一標識。 - 請求被髮送到
rpc_queue
佇列。 - RPC 工作者(即伺服器)在該佇列上等待請求。一旦收到請求,它將完成任務,並透過
replyTo
欄位指定的佇列將結果傳送回客戶端。 - 客戶端在回覆佇列中等待資料。當訊息到達時,它檢查
correlationId
屬性。如果匹配請求中的值,它將響應返回給應用程式。
實現全流程
斐波那契任務:
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2);
}
我們定義了斐波那契函式。該函式假設只接收有效的正整數輸入。(對於較大數字,該演算法效率較低,它可能是最慢的遞迴實現。)
伺服器程式碼可在此處找到:RPCServer.java。
客戶端程式碼略顯複雜,完整的示例原始碼可參考 RPCClient.java。
編譯並設定類路徑:
javac -cp $CP RPCClient.java RPCServer.java
我們的 RPC 服務現在已準備就緒。啟動伺服器:
java -cp $CP RPCServer
# => [x] 正在等待 RPC 請求
要請求斐波那契數,執行客戶端:
java -cp $CP RPCClient
# => [x] 請求 fib(30)
此處展示的設計並非 RPC 服務的唯一實現方式,但它有以下優勢:
- 如果 RPC 伺服器太慢,你可以透過執行另一個伺服器例項進行擴充套件。試著在新的控制檯中執行第二個
RPCServer
。 - 在客戶端,RPC 只需傳送和接收一條訊息。無需像
queueDeclare
這樣的同步呼叫。因此,RPC 客戶端只需一個網路往返即可完成一次 RPC 請求。
程式碼仍然相對簡單,並未嘗試解決更復雜但重要的問題,如:
- 如果沒有伺服器執行,客戶端應該如何響應?
- RPC 是否需要某種超時機制?
- 如果伺服器發生故障並引發異常,是否應該將其轉發給客戶端?
- 在處理訊息前,是否應檢查其有效性(如範圍、型別)以防止無效訊息的進入?
關注我,緊跟本系列專欄文章,咱們下篇再續!
作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。
各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。
負責:
- 中央/分銷預訂系統效能最佳化
- 活動&券等營銷中臺建設
- 交易平臺及資料中臺等架構和開發設計
- 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
- LLM Agent應用開發
- 區塊鏈應用開發
- 大資料開發挖掘經驗
- 推薦系統專案
目前主攻市級軟體專案設計、構建服務全社會的應用系統。
參考:
- 程式設計嚴選網
本文由部落格一文多發平臺 OpenWrite 釋出!