非同步API中事件、命令和狀態區別

banq發表於2024-04-29


事件、命令、狀態和時間序列值的區別:

事件:   

  • 使用者已建立
  • ECS例項已啟動

命令/說明/請求 :
  • 向使用者 X 傳送重置密碼電子郵件
  • 從使用者 Y 處收取 £x 的付款

狀態  :  
  • 使用者(完整物件)
  • 產品(完整的物件)
  • 訂單(完整物件)

時間序列值
  • 股票價格
  • API 上的錯誤率指標
  • 時間序列資料當然是單個事物的狀態,它是獨特的,因為它具有周期性——無論它是否發生變化,都會傳送更新。

事件與狀態
事件表示某些事情已經發生或改變,例如“帳戶已建立”。忽略時間戳和後設資料,它可能具有如下有效負載:

{
  <font>"EventType": "ACCOUNT_CREATED"
 
"AccountID": "8c0fd83f-ff3f-4e0e-af4b-2b7470334efa"
}

如果您想了解特定帳戶的詳細資訊,那麼您需要透過其他途徑獲取它,例如對帳戶 REST API 的 HTTP 請求或任何現有的介面。

另一方面,狀態包含已建立或更改的任何實體的完整狀態。例如

{
  <font>"EntityType": "ACCOUNT"
 
"ID": "8c0fd83f-ff3f-4e0e-af4b-2b7470334efa",
 
"Name": "David",
 
"Email": "An email@domain.com",
 
"Tel: {
   
"Type: "Mobile"
   
"Country Code": "44"
   
"777777777"
  }
  .....etc
}

在這個例子中,我沒有加入一個欄位來說明是建立還是更新。下游並不一定會關心他們是否看到了之前的資訊,他們只會檢查他們是否已經擁有了特定的實體。我們傳送的是狀態,而不是發生了什麼,比如建立或更新。例外情況是需要特殊處理的刪除,例如使用特殊的報文型別或空有效負載來表示狀態已消失。

狀態資訊可用於多種場景,但在使用事件日誌而非資料庫作為真相來源的事件源路線時,狀態資訊則是必需的。

我在實踐中發現,狀態和事件之間的區別可能比目前建議的要模糊一些。你可能會遇到半途而廢的解決方案,即事件包含一些常用資訊(如電子郵件),但不包含更詳細的資訊。這樣做雖然不是很純粹,但對於只關心電子郵件的使用者來說,可以節省大量的 API 請求。

與此類似,有時事件只傳達一個欄位的變化,例如 "電話號碼已更改 "事件就包括電話號碼和使用者 ID,因此包含了所有狀態。有時,狀態資訊可能包括前後狀態或包含已更改欄位名稱/路徑(如 changes=[firstname,person.phone.mobile])的更改列表。

內容比較:

  • 事件:一個 ID 和列舉來說明發生了什麼  
  • 狀態: 完整的資料實體

型別:

  • 事件:    標識發生的特定事件的列舉,例如 EMAIL_UPDATED  
  • 狀態: 實體名稱,如 PROFILE

後續呼叫:

  • 事件:需要後續呼叫,後續需要API呼叫 
  • 狀態: 不需要

更新  :

  • 事件:  透過事件名稱標識,但無法檢視舊值    
  • 狀態:如果狀態是新的或更新的,通常不會傳達,但有時訊息會有舊的和新的狀態

訊息大小:

  • 事件:    小的
  • 狀態:    中型或大型

事件與狀態的關鍵區別:

  • 狀態:以名詞靜態為主,當前狀態,通常需要主語,主謂賓中的賓語是狀態。
  • 事件:以動詞為主,突出發生了什麼事情,以謂語為主,可以沒有主語。


權衡
具體權衡標準:

1、消費者數量
隨著使用者數量的增加,有狀態方法的優點是您不會在 API 上承受沉重的負載。
想象一下,匯流排上有 100 條訊息,同時到達 15 個消費者。然後,您的 API 將在一兩秒內收到 1500 個請求。

2、彈性
就消費者數量而言,如果您的 API 不是那麼可靠,那麼有狀態選項可以更好地提高彈性,因為您不依賴訊息匯流排和 API,只需依賴訊息匯流排即可獲取所有資訊資料。

3、耦合
彈性和其他一些要點實際上是一種耦合形式。如果一個服務必須呼叫另一個服務的 API 來獲取資料,那麼它與該服務的耦合比狀態訊息解決方案更緊密,在狀態訊息解決方案中,消費者不需要了解生產者,也不依賴於其名稱、彈性、API 模式等。

4、資料傳輸量
如果大多數消費者只需要 2 或 3 個欄位,但狀態訊息中有 200 個欄位,則可能會造成浪費。在這種情況下,假設同步 API(例如 REST、GraphQL)粒度更細,事件選項將更加高效。對於小型集中狀態物件(例如 10-20 個欄位)來說,這不是一個主要優點,但如果傳送大約 10 KB 的大塊資料,則更重要。

5、消費者的簡單性
有時我聽到有人斷言,狀態資訊更簡單,因為不需要呼叫應用程式介面(API)。但是......並不總是那麼簡單。最好的解釋就是舉個例子。請考慮以下情況:

  • 感興趣的資料是使用者的賬戶詳情
  • 當賬戶屬性發生變化(如電子郵件地址發生變化)時,出於安全考慮,您希望向使用者傳送電子郵件或簡訊
  • 您採用了狀態資訊的方式
  • 不包含更改列表,只包含當前狀態。

任何負責傳送電子郵件或簡訊的服務都必須有自己的狀態,這樣它才能比較前後的值,發現是電子郵件發生了變化,而不是姓名等其他欄位發生了變化。

另一方面,如果您有一個 "電子郵件已更改 "的單一事件(事件中包含新郵件或可透過 API 獲取),那麼處理服務就可以是無狀態的。

在這種情況下,使用事件的消費者實際上要簡單得多,但狀態資訊的問題可以透過包含更改列表來解決。

6、資料結構管理
對於有狀態方法(REST 和訊息),您必須保持兩個schema結構同步,與 API 框架相比,很多訊息傳遞系統對schema管理的支援並不好。

7、聚合
如果一個服務需要幾個實體來完成其工作(這些實體通常透過多條狀態訊息到達),那麼事件模型可能會更簡單。消費者接收到一個事件,然後立即進行幾個 REST 呼叫或單個圖形 QL 呼叫,以獲取所需的實體來繼續工作。

使用狀態方法時,您可能需要處理順序混亂的訊息,並在繼續處理之前等待所有訊息。或者,必須構建一個包含所有實體的更大的聚合狀態訊息,這也有其自身的問題。

什麼是指令/命令/請求
指令或命令是 "做 X "的請求。舉例來說,一個商業網站或政府服務機構會在使用者付款後透過郵局或快遞送貨。非同步操作有兩種方式:

  • 有一個交付微服務正在監聽通用的 ORDER_PLACED 事件(或訂單狀態),並根據這些事件安排交付。
  • 訂單應用程式(或消耗 ORDER_PLACED 事件的中間微服務)向配送公司服務寫出 "PREPARE_DELIVERY "指令或類似指令。
  • 後者是指令的一個示例。

指令資訊通常包含下游工作所需的全部資訊,但也並非必須如此。一般來說,由於指令具有很強的針對性,因此沒有理由不在報文中包含相關資料,除非需要任何最好不要在報文匯流排上傳輸的大檔案或影像。

命令與狀態以及事件的區別
在瞭解了命令之後,我們來比較一下狀態和事件資訊。我認為它們的區別在於

  • 狀態或事件訊息非常通用,可能有多個服務對其感興趣。
  • 指令命令更具體,針對的是某個特定的使用者,儘管耦合度較低(透過佇列或類似方式)。
  • 對於命令,人們通常期望透過另一條訊息得到響應,以確認該訊息已被接收、接受或執行。

我個人對此的看法是,命令最適合工作流,在這種工作流中,你希望保持較低的耦合度,但無論如何,你都在請求某些事情發生,而且你關心它是否真的發生。您可能希望能夠在儀表板上顯示使用者訂單的狀態及其交付情況,並在出現問題時採取行動。您不希望從眾多系統中調取資料來獲取該檢視。這種情況通常需要一個協調器,如 Camunda 或 Uber Cadence 或 AWS Step Functions。

對於事件/狀態資訊,源系統(或協調器)在完成其工作後不承擔任何責任。它只需釋出一條訊息,說明 "這裡有一些新的/更新的資料",然後就可以繼續工作了。其他服務則負責決定該做什麼,並提供下游操作的狀態檢視。一個顯而易見的推論是,在傳輸狀態時,如果任何關鍵(業務功能)下游都依賴於它,那麼訊息傳遞系統就必須非常強大,因為源系統中沒有重試或標記錯誤的機會。源系統不知道下行系統是否收到了資料,也不知道下行系統是否收到了資料併成功處理了它。

時間序列資料
在這裡我不想多說,因為在資訊中寫什麼的問題要明顯得多:

  • 1 個或多個值、
  • 值型別
  • 時間戳。

所面臨的挑戰主要圍繞資訊匯流排和消費者,例如,確定在給定時間內所有資料何時到達(參見流媒體系統中的水印),以及在資料丟失風險與吞吐量和延遲之間找到適當的平衡。但在資訊中加入什麼內容的問題相對簡單。

除了有效負載的內容之外,還應該考慮構成所有訊息的標準信封的訊息後設資料。一些建議是:

ID
在訊息中包含唯一的 ID,無論它是狀態、命令等。我建議使用 UUID 來保證唯一性。該 ID 應該僅與訊息有關,而不是與實體有關。這很有用,因為:

  • 對於命令,操作可能不是冪等的,例如傳送電子郵件不是冪等的,因此您必須能夠消除重複
  • 即使對於理想情況下冪等的狀態,最好避免在消費者中重複工作,因此有一個 ID 來檢查可以讓這變得容易。

時間戳
在標準 UTC 格式中包含時間戳,以便消費者可以重新排序訊息並清楚時間戳的含義。我建議這是基於寫入源資料庫(如果適用)或處理的實體,而不是訊息傳送時間,這線上程系統中可能是不確定的。
在格式上,這是有爭議的,但我更喜歡時間戳的字串版本,因為它使除錯更容易,而無需轉換紀元值。例如 2024-02-19T12:18:07.000Z,而不是 1708344420000。

版本控制
如果您希望能夠輕鬆地將不同版本路由到不同的使用者,請制定版本控制計劃,該計劃可以位於版本欄位中,也可以作為訊息型別名稱的一部分。
不要混淆訊息信封(在許多實體之間共享)的版本和特定實體的版本。最好有 2 個版本號,每個版本號一個。

測試和環境
在訊息中允許測試和多種環境是值得的。例如,考慮一個標誌來說明訊息是否是測試訊息。這將允許輕鬆過濾生產中的測試資料,而不會汙染您的分析系統。

還要考慮環境標誌。將生產資料流入測試環境以幫助提供真實的資料是很常見的。有時您會想了解這一點,因為資料來自生產,引用的 ID 將不存在。標誌可以讓您知道這來自另一個環境,並且並非所有連結的資料都可能流入該測試環境。

例子
作為具有上述欄位的訊息的示例:

 {
  <font>"messageID": "cc7b9901-c339-4c7d-80cd-c400f20581fd"
 
"timestamp": "2024-02-19T12:18:07.000Z"
 
"entityType": "ACCOUNT",
 
"envelopeVersion": 1,
 
"isTest": true,
 
"fromEnvironment": "prod"
 
"payload": {
   
"version" = 1,
   
"accountID": "0a0ebe8d-e48a-4195-8372-4f54c5dfd4e5",
  }
 }

最後的想法
我們已經瞭解了事件與狀態的一些優缺點,並且還研究了命令,觀察到後者經常用於工作流程中,在該工作流程中您關心指令的接收並希望瞭解操作的狀態。它的後面。

具體就狀態和事件而言,我不確定是否存在 100% 首選的方法,只是根據消費者數量、資料實體之間的關係進行權衡。如果我必須離開圍欄,我想說的是,事實證明,國家資訊往往比預期更復雜,所以在其他條件相同的情況下,我稍微傾向於事件。有幾個原因:

  • 只有一個 API 用於獲取資料 - 不需要保持 2 個同步
  • 消費者不必組裝按隨機順序出現的物品
  • 可透過單一 API 訪問的單一事實來源
  • 無需擔心重播和回填 - 只需從 REST/GraphQL/RPC API 獲取歷史資料。

儘管如此,事件確實意味著服務之間的耦合更緊密,並且如果消費者數量很高,則不會總是擴充套件。

無論你做什麼,都要有一個清晰的計劃,儘量保持一致和合乎邏輯,不要意外地做出選擇。換句話說,不要在沒有任何明確推理的情況下隨機混合服務中的指令、狀態和事件。這並不意味著您應該嘗試採用一種適合所有企業範圍的模式。即使在單個域中,也可以讓一個服務發出狀態,而另一個服務監聽該狀態並在資料更改時傳送命令以執行特定操作。

相關文章