API設計中使用命令模式替代RPC

banq發表於2024-12-25


我們已經擁有眾多 API 架構風格,例如 REST、RPC和 SOAP 等,將命令模式新增到其中具有以下優勢,它可以成為這一角色的良好候選者:

  • 提供一種對交易事務進行建模的方法。命令共享一個通用介面,並且可以一次執行多個操作,因此非常適合此目的。
  • 允許將使用者操作儲存為命令列表,這對於記錄使用者活動、重放操作或實施審計機制很有用。
  • 提供撤消/重做功能。
  • 遵循開放/封閉原則。可以輕鬆新增新命令,而無需修改現有程式碼。
  • 增強可測試性,因為可以單獨測試命令。

命令模式與 RPC比較
因為它們很相似:兩種方法都涉及在伺服器上執行任意操作。

一、概念不同
最明顯的區別是:

  • 命令模式透過命令進行操作
  • 而 RPC 依賴於函式。

二、類似表述
儘管如此,它們在透過網路傳輸時看起來是一樣的:

Cmd { prop1, prop1, … }   -> cmd_type, prop1, prop2, … 
Fn   ( param1, param2, … )   -> fn_id, param1, param2, …

三、每次執行的運算元
與Command命令模式不同,RPC每次只能執行一個動作,不太方便,我們看下面的函式組成:
bar(foo())

RPC 建議發出兩個請求或使用類似foobar 的函式來減少往返次數,後一種選擇並不理想。

降低延遲問題的願望又會影響通訊介面,這個兩難問題很廣泛和複雜,所以,Cap'n Proto提供了自己的解決方案 solution ,更詳細地描述了這個問題。

另一方面,函式組合對於命令模式來說不是問題:


FooBarCmd
  Exec(r Receiver) { r.bar(r.foo()) }

  
一般來說,它允許透過單個請求執行無限數量的操作,而不會增加介面複雜性或影響效能。

四、函式內部的命令
RPC 實際上可以使用命令模式來實現。在這種情況下,函式可以簡單地向伺服器傳送命令:

func foo() { send FooCmd }
func bar() { send BarCmd }
func foobar() { send FooBarCmd }

因此,我們只需要知道如何處理命令。這些知識用途更廣泛,甚至可以改進現有的 RPC 系統。

命令模式挑戰
在提供更靈活和通用的抽象的同時,命令模式也引入了一些挑戰:

1、需要伺服器主動以某種方式區分一個命令與另一個命令。

  • 解決方案:在每個命令之前,傳送其型別。

2、命令必須返回結果,並且每個命令都可以有自己的結果。

  • 解決方案:命令本身可以負責返回一個或多個結果,而不是返回結果。

Invoker
  r Receiver
  Invoke(cmd Cmd, t Transport) {
    cmd.Exec(r, t) // 透過使用Transport, 命令傳送回結果the command sends results back.
    ...
  }

3、命令在執行過程中可能會出錯,如何處理?
解決方案:

  • 如果想讓客戶端知道這個錯誤,命令可以返回錯誤結果。
  • 另一種情況是,命令可以終止與客戶端的連線,並向呼叫者返回錯誤。


Invoker 
  r Receiver 
  Invoke (cmd Cmd, t Transport) error { 
    err = cmd. Exec (r, t) // 如果 Exec() 方法返回錯誤,則
    // 與客戶端的連線將終止。這正是
    // 命令在傳送結果失敗後可能執行的操作。
     ... 
  }

為了限制其執行時間,命令必須知道伺服器何時收到它。此時間可能與執行開始時間不同。解決方案:命令可以將其作為引數接收。

Invoker 
  r Receiver 
  Invoke (at Time, cmd Cmd, t Transport) error { 
    err = cmd.Exec ( at, r, t) // 'at' 參數列示收到命令的時間。     ...   }

    

這就是適合我們需求的命令模式的樣子。

要看到它的實際作用,我們需要考慮另外一件事。
序列化格式
要將資料傳送到某個地方,必須先將其轉換為位元組序列。這可以透過多種方式完成,這就是為什麼存在如此多的序列化格式。需要考慮的最重要的指標之一是格式使用的位元組數。我們需要透過網路傳輸的位元組數越少,我們的應用程式就會越快。

MUS 格式就是基於這些想法而建立的。它幾乎不使用後設資料,實際上是一種相當簡單的格式。我不想重複太多,所以這裡有一個 link 連結,你可以在那裡閱讀更多相關資訊。

這就是理論的全部內容。

具體實現
上述想法已經以兩個庫的形式在 Golang 中實現:cmd-stream-go 和 mus-go。

1、cmd-stream-go 
cmd-stream-go是一個高效能客戶端-伺服器庫,它實現了命令模式並且:

  • 可以透過 TCP、TLS 或相互 TLS 工作。
  • 具有非同步客戶端,它僅使用一個連線來傳送命令和接收結果。
  • 支援伺服器流式傳輸,即一個命令可以返回多個結果(不直接支援客戶端流式傳輸,但也可以實現)。
  • 支援重新連線功能。
  • 支援保活功能。
  • 可以與各種序列化格式一起使用。
  • 具有模組化架構。


2、mus-go
mus-go是一個 MUS 格式的序列化器,它:

  • 表示一組序列化原語,不僅可用於實現 MUS,還可用於實現其他序列化格式。
  • 有流媒體版本。
  • 可以在 32 位和 64 位系統上執行。
  • 可以在解組時驗證和跳過資料。
  • 支援指標。
  • 可以序列化圖形或連結串列等資料結構。
  • 支援資料版本控制。
  • 支援 oneof 功能。
  • 支援無序反序列化。
  • 支援零分配反序列化。

此外,正如您在基準測試 benchmarks中所看到的,它表現出了出色的效能。

cmd-stream-go/MUS 比 gRPC/Protobuf快 3 倍左右。

概括
傳送命令是一個非常好的抽象。它類似於 RPC,但並不限制我們只能執行一項操作。此外,命令模式既可以替代 RPC,也可以用作構建 RPC 的工具。它還提供了上述幾個優點,並且已經具有高效能實現。所有這些都使命令模式成為 API 架構風格的絕佳選擇。

 

相關文章