使用發件箱模式實現微服務的Saga編排

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"核心要點"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Saga能夠實現長時間運行的、分佈式的業務事務,這樣的事務會跨多個微服務執行一組操作,實現一致的全有或全無的語義。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了實現解耦,微服務之間的通信最好按照異步的方式來進行,比如藉助Apache Kafka使用分佈式的提交日誌。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"發件箱模式爲服務作者提供了一種解決方案,能夠讓他們在本地數據庫執行寫入,同時通過Apache Kafka發送消息,避免依賴不安全的“雙重寫入(dual writes)”。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Debezium是一個分佈式的開源數據變更捕獲平臺,爲使用發件箱模式的編排式Saga流提供了健壯和靈活的基礎。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在轉向微服務的時候,我們意識到的第一件事情就是單個服務都不是孤立存在的。儘管我們的目標是創建松耦合、獨立的服務,它們之間的交互要越少越好,但是很可能某個服務需要另外一個服務所持有的數據集,或者多個服務需要協同行動才能達成業務領域中一致的操作結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/blog\/2019\/02\/19\/reliable-microservices-data-exchange-with-the-outbox-pattern\/","title":"","type":null},"content":[{"type":"text","text":"變更數據捕獲"}]},{"type":"text","text":"實現的發件箱模式是解決微服務之間數據交換問題的一種行之有效的方式,這種模式能夠避免對多種資源(如數據庫和消息代理)的不安全的“雙重寫入”,從而能夠實現最終一致的數據交換,在這個過程中不依賴所有參與者的同步可用性,也不需要複雜的協議,如XA(由"},{"type":"link","attrs":{"href":"https:\/\/www.opengroup.org\/","title":"","type":null},"content":[{"type":"text","text":"The Open Group"}]},{"type":"text","text":"定義的廣泛用於分佈式事務處理"},{"type":"link","attrs":{"href":"https:\/\/pubs.opengroup.org\/onlinepubs\/009680699\/toc.pdf","title":"","type":null},"content":[{"type":"text","text":"標準"}]},{"type":"text","text":")。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本文中,我會探討如何進一步使用發件箱模式,也就是將其用於實現Saga,即可能會跨多個微服務的長時間運行的事務。常見的例子就是預訂由多個部分組成的行程:要麼所有的航班和住宿都預訂成功,要麼全部取消預訂。Saga將這樣一個整體的業務事務分割成一系列的本地數據庫事務,這些事務會在相關的服務中執行。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Saga入門"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了在出現失敗的情況下“回滾”整體的業務事務,Saga依賴於補償事務的理念:每個在此之前已經應用過的本地事務必須要能通過運行另外一個事務來進行“撤銷”,該事務會取消掉之前已經完成的變更。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Saga並不是"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/History-of-Extended-Transactions\/","title":"","type":null},"content":[{"type":"text","text":"什麼新鮮的概念"}]},{"type":"text","text":":早在1987年,Hector Garcia-Molina和Kenneth Salem在他們的SIGMOD "},{"type":"link","attrs":{"href":"https:\/\/www.cs.cornell.edu\/andru\/cs711\/2002fa\/reading\/sagas.pdf","title":"","type":null},"content":[{"type":"text","text":"Sagas"}]},{"type":"text","text":"論文中就首次討論了這個理念。但是,在業界不斷向微服務架構演進的背景下,Saga作爲一種由相關服務中的本地事務作爲支撐的方案越來越受歡迎,比如目前正在活躍開發中的"},{"type":"link","attrs":{"href":"https:\/\/github.com\/eclipse\/microprofile-lra","title":"","type":null},"content":[{"type":"text","text":"長時間運行的操作的MicroProfile規範"}]},{"type":"text","text":",這些問題通常"},{"type":"link","attrs":{"href":"https:\/\/www.theserverside.com\/news\/1365143\/ACID-is-Good-Take-it-in-Short-Doses","title":"","type":null},"content":[{"type":"text","text":"不能使用ACID語義來解決"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了讓闡述更加具體,我們考慮一個電子商務業務的樣例,它由三個服務組成:訂單、消費者和支付。當新的購買訂單提交到訂單服務時,就會執行如下的流程,其中包含了其他的兩個服務:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/31\/01\/3157f599243f365a8d783dc7106ae801.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖1:訂單狀態的轉換"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們需要通過消費者服務來檢查傳入的訂單是否匹配消費者的信用額度(因爲我們不希望用戶的待處理訂單超過某個閾值)。如果消費者的信用限額是500美元,新進來的訂單是300美元,那麼這個訂單就符合當前的限額,剩餘的額度就會變成200美元。如果隨後又有一個259美元的訂單,那麼它就會被相應的拒絕,因爲它超過了當前消費者開放的信用額度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果信用額度檢查成功的話,那麼就需要通過支付服務申請對訂單進行付款。如果信用額度檢查和支付請求都成功的話,訂單將會轉移至"},{"type":"codeinline","content":[{"type":"text","text":"Accepted"}]},{"type":"text","text":"狀態,這樣就可以對該訂單開始進行交付了(這一步驟不在我們這裏所討論的過程之中)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,如果信用額度檢查失敗的話,訂單會立即轉移至"},{"type":"codeinline","content":[{"type":"text","text":"Rejected"}]},{"type":"text","text":"狀態。如果這個步驟成功了,但是後續的支付請求失敗了,在將訂單轉移至"},{"type":"codeinline","content":[{"type":"text","text":"Rejected"}]},{"type":"text","text":"狀態之前,需要將在前面步驟中分配的信用額度釋放掉。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"可選的實現方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現分佈式Saga的時候,有兩種通用的方式,即協同式(choreography)和編排式(orchestration)。在協同式Saga中,每個參與其中的服務都會在它執行完本地事務之後發送一條消息給下一個服務。而在編排式Saga中,會有一個協調服務,它會逐個調用參與其中的每個服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩種方式都有其優點和缺點(請參見Chris Richardson的"},{"type":"link","attrs":{"href":"https:\/\/chrisrichardson.net\/post\/sagas\/2019\/08\/04\/developing-sagas-part-2.html","title":"","type":null},"content":[{"type":"text","text":"博客文章"}]},{"type":"text","text":"以及Yves do Régo的"},{"type":"link","attrs":{"href":"https:\/\/medium.com\/@ydorego\/microservices-orchestration-vs-choreography-the-eternal-saga-d58c35e07d81","title":"","type":null},"content":[{"type":"text","text":"文章"}]},{"type":"text","text":"以瞭解更詳細的討論)。就我個人而言,我更加喜歡編排式,因爲它定義了一箇中心點(編排器,或者稱爲“Saga執行協調器”,簡稱SEC),通過它我們能夠查詢得到特定Saga的當前狀態。它能夠避免各個參與者之間點到點的通信(編排者除外),而且還允許在流程中添加額外的中間步驟,這個過程中並不需要調整每個參與者。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在深入實現這個Saga流程之前,我們有必要花點時間討論一下Saga所提供的事務語義。我們首先看一下Saga如何滿足事務的四個經典ACID屬性,這是Theo Härder和Andreas Reuter(基於Jim Gray早前的"},{"type":"link","attrs":{"href":"http:\/\/jimgray.azurewebsites.net\/papers\/thetransactionconcept.pdf","title":"","type":null},"content":[{"type":"text","text":"工作成果"}]},{"type":"text","text":")在他們的基礎論文"},{"type":"link","attrs":{"href":"https:\/\/citeseerx.ist.psu.edu\/viewdoc\/summary?doi=10.1.1.115.8124","title":"","type":null},"content":[{"type":"text","text":"Principles of Transaction-Oriented Database Recovery"}]},{"type":"text","text":"中所定義的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原子性(Atomicity)✅:該模式能夠確保所有的服務要麼應用本地事務,要麼在出現故障的時候,所有已執行的本地事務都能得到補償,所以不會產生任何有效的數據變化。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一致性(Consistency)✅:在成功執行組成Saga的所有事務之後,所有的本地約束都能確保得到滿足,從而使整個系統從一個一致狀態轉換到另外一個一致的狀態。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隔離性(Isolation)❌:儘管Saga有最終失敗的可能性,這會導致所有之前已經執行的事務被補償,但是鑑於在Saga的運行過程中,本地事務已經進行了提交,所以它們的變更已經對其他併發事務可見了,換句話說,從整個Saga的角度來看,隔離級別可以類比成”讀取未提交的數據(read uncommitted)“。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"持久性(Durability)✅:一旦Saga的本地事務得到提交,它們的變更就會持久化,並且會持久性保存,比如能夠經歷服務的故障或重啓。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從服務消費者角度來看,例如某個用戶通過訂單服務提交了一個購買訂單,系統最終是一致的,也就是說,根據不同的參與其中的服務的邏輯,要耗費一定的時間購買訂單才能處於正確的狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至於參與其中的服務之間的通信,它可以是同步進行的,如通過HTTP或"},{"type":"link","attrs":{"href":"https:\/\/grpc.io\/","title":"","type":null},"content":[{"type":"text","text":"gRPC"}]},{"type":"text","text":",也可以異步進行,比如通過消息代理或分佈式日誌,如"},{"type":"link","attrs":{"href":"https:\/\/kafka.apache.org\/","title":"","type":null},"content":[{"type":"text","text":"Apache Kafka"}]},{"type":"text","text":"。只要有可能,我們就應該優先使用異步的方式進行服務間的通信,因爲它將發送服務與消費服務的可用性進行了解綁。正如我們在下一節所看到的,藉助變更數據捕獲,即便是Kafka本身的可用性都不再是什麼問題。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"回顧發件箱模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,發件箱模式和變更數據捕獲(由"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/","title":"","type":null},"content":[{"type":"text","text":"Debezium"}]},{"type":"text","text":"提供)是如何將這一切組織在一起的呢?如前文所述,Saga協調器最好通過請求和答覆消息通道與相關服務進行異步的通信。Apache Kafka是實現這種通道的一個非常流行的可選方案。但是,編排器(以及每個參與其中的服務)還需要將事務應用到其特定的數據庫中,從而執行整個Saga流中屬於它們的那一部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然簡單地執行某個數據庫事務,並且稍後發送一條對應的消息到Kafka是一種非常誘人的做法,但是這並不是一個好主意。這兩個動作橫跨數據庫和Kafka,因此並不會在同一個事務中完成。我們遲早會遇到不一致狀態的問題,比如數據庫事務已經提交了,但是寫入到Kafka的過程失敗了。但是,"},{"type":"link","attrs":{"href":"https:\/\/speakerdeck.com\/gunnarmorling\/practical-change-data-streaming-use-cases-with-apache-kafka-and-debezium-qcon-san-francisco-2019?slide=10","title":"","type":null},"content":[{"type":"text","text":"好朋友是不會讓自己的朋友進行雙重寫入的"}]},{"type":"text","text":",發件箱模式提供了一個非常優雅的方式來解決這個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/53\/96\/53e86e8a829bc082c703f3a038411196.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖2:安全地更新數據庫並通過發件箱模式發送消息到Kafka"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們不會在更新數據之後直接發送消息,而是讓服務基於同一個事務執行正常的更新並將消息插入到數據庫中一個特定的發件箱表中。因爲這個操作是在同一個數據庫事務中完成的,我們會有兩種結果,要麼服務模型的變更會得到持久化並且消息能夠安全地保存到發件箱表中,要麼這兩個都不會得到執行。事務寫入到數據庫的事務日誌之後,Debezium數據變更捕獲進程就會從這裏得到發件箱的消息,並將其發送至Apache Kafka。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是通過使用“至少執行一次(at-least-once)”的語義實現的:在特定的環境下,相同的發件箱消息可能會多次發送到Kafka中。爲了讓消費者探測到並忽略重複的消息,每條消息應該有一個唯一的id。例如,這可以是一個UUID,也可以是一個單調遞增的序列,這是與每個消息生產者密切相關的,這個id應該通過Kafka消息的頭信息進行傳播。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"通過發件箱模式實現Saga"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工具箱中的發件箱模式準備就緒之後,接下來的事情就更清楚了。訂單服務將作爲Saga協調者,在接收到下單的請求之後(通常會通過REST API實現),它會通過更新本地狀態(包括持久化訂單模型和Saga執行日誌)來觸發整個流程,並依次發送消息給其他兩個參與其中服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩個服務對通過Kafka接收到的消息做出反應,執行本地事務來更新它們的數據狀態並且通過它們自己的發件箱表向協調者發送一個答覆消息。整個解決方案看起來如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/2e\/df\/2ed6d0284d82a6b3e3d67c9c14b2ecdf.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖3:使用發件箱模式的Saga編排"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Debezium的GitHub"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga","title":"","type":null},"content":[{"type":"text","text":"樣例倉庫"}]},{"type":"text","text":"中,你可以看到這個架構的完整概念驗證(proof-of-concept,PoC)實現。該架構的主要組成部分如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"三個服務,分別是"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga\/order-service","title":"","type":null},"content":[{"type":"text","text":"訂單"}]},{"type":"text","text":"(用來管理購買訂單並作爲Saga協調者)、"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga\/customer-service","title":"","type":null},"content":[{"type":"text","text":"消費者"}]},{"type":"text","text":"(用來管理消費者的信用限制)和"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga\/payment-service","title":"","type":null},"content":[{"type":"text","text":"支付"}]},{"type":"text","text":"(用來處理信用卡支付),每個服務都有自己的數據庫(Postgres)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Apache Kafka作爲消息傳輸的骨架"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Debezium運行在Kafka Connect之上,它會訂閱這三個不同數據庫的變更,並通過Debezium的"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/documentation\/reference\/configuration\/outbox-event-router.html","title":"","type":null},"content":[{"type":"text","text":"發件箱事件路由(outbox event routing)"}]},{"type":"text","text":"組件將它們發送至對應的Kafka主題。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這三個服務是使用"},{"type":"link","attrs":{"href":"https:\/\/quarkus.io\/","title":"","type":null},"content":[{"type":"text","text":"Quarkus"}]},{"type":"text","text":"實現的,這是一個構建雲原生微服務的技術棧,構建出來的應用可以運行在JVM上,也可以編譯成原生二進制(通過GraalVM實現)。當然,這個模式也可以通過其他的技術棧或語言來實現,只要它們提供消費來自Kafka消息並且寫入數據庫的能力即可。另外,組合不同的實現技術也是可行的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏涉及到四個Kafka主題:信用審批消息的請求和響應主題以及支付消息的請求和響應主題。在Saga執行成功的情況下,恰好會有四條消息會被進行交換。如果其中的某一個步驟失敗的話,就需要一個補償事務了,在每個步驟都會有額外的請求和響應消息對來進行補償。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"確保順序"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了達到擴展的目的,Kafka主題可以組織到多個分區中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有在一個分區內部,才能確保消費者接收到消息的順序與生產者發送消息的順序完全一致。默認情況下,具有相同key的所有消息都會發送到相同的分區中,所以Saga的唯一id是Kafka消息key的自然選擇。通過這種方式,同一個Saga實例的消息就能保證以正確的順序進行處理。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們有多個Saga實例,它們用於Saga消息交換的主題出現在了不同的分區中,那麼它們可以並行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/6b\/83\/6b3735535c9881d6a35090f592742c83.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖4:成功Saga流的執行序列"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個服務都通過自己數據庫中的發件箱表發送消息。在這裏,這些消息由Debezium捕獲併發送至Kafka,最終由接收消息的服務進行消費。在發送和消息的時候,訂單服務作爲編排者也會將Saga的進度持久化到本地狀態表中(後文詳解)。另外,所有的參與者會將它們所消費的消息的id記錄到一個journal表中,從而標識後續可能會出現的重複。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,我們現在考慮一下,如果流中的某個步驟失敗了會怎麼樣?假設支付步驟因爲消費者的信用卡已經過期而失敗了。在這種情況下,在前面消費者服務中已經預留的信用卡額度需要再次進行釋放。爲了實現這一點,訂單服務會向消費者服務發送一個補償請求。將這個過程放大一點(就像前面介紹Debezium和Kafka詳情那樣),那麼消息交換將會如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/89\/d8\/89921c2f16b35bf3808a70214584cdd8.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖5:帶有補償的Saga流的執行序列"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"討論完服務之間的消息流之後,接下來我們深入訂單服務的一些實現細節。概念驗證實現以簡單狀態機的形式提供了一個通用的Saga編排器以及針對訂單場景的Saga實現,在後文中我們會對其進行深入討論。訂單服務實現的”框架“部分在"},{"type":"codeinline","content":[{"type":"text","text":"sagastate"}]},{"type":"text","text":"表中跟蹤了Saga執行的當前狀態,其模式如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/0a\/85\/0a958bf73091926c7ff113c9fd122785.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖6:Saga狀態表的模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個表滿足了Saga日誌的要求。它的每個列如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"id"}]},{"type":"text","text":":給定Saga實例的唯一標識符,代表創建一個特定的購買訂單。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"currentStep"}]},{"type":"text","text":":Saga當前所處的步驟,如“credit-approval”或“payment”。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"payload"}]},{"type":"text","text":":與特定Saga實例相關聯的任意的數據結構,例如,在Saga生命週期中,包含相對應的購買訂單的id和其他有用的信息;儘管在樣例實現中我們使用JSON作爲載荷的格式,但是也可以考慮使用其他的格式,比如"},{"type":"link","attrs":{"href":"https:\/\/avro.apache.org\/","title":"","type":null},"content":[{"type":"text","text":"Apache Avro"}]},{"type":"text","text":",並將載荷的模式存放到模式註冊表中。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"status"}]},{"type":"text","text":":Saga當前的狀態,可以是"},{"type":"codeinline","content":[{"type":"text","text":"STARTED"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"SUCCEEDED"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"ABORTING"}]},{"type":"text","text":"或"},{"type":"codeinline","content":[{"type":"text","text":"ABORTED"}]},{"type":"text","text":"其中之一。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"stepState"}]},{"type":"text","text":":字符串化之後的JSON結構,描述了每個步驟的狀態,比如\"{\"credit-approval\":\"SUCCEEDED\",\"payment\":\"STARTED\"}\""}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"type"}]},{"type":"text","text":":Saga命名化的類型,比如“order-placement”,用來區分系統中所支持的不同類型的Saga。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"version"}]},{"type":"text","text":":一個基於樂觀鎖的版本,用來探測和拒絕對一個Saga實例的併發更新(在這種情況下,需要重試那些觸發失敗更新的消息,從Saga日誌中重新加載當前的狀態)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當訂單服務發送請求到消費者和支付服務並通過Kafka接收到它們的答覆時,Saga狀態就會更新到這個表中。通過搭建Debezium connector來跟蹤"},{"type":"codeinline","content":[{"type":"text","text":"sagastate"}]},{"type":"text","text":"表,我們可以很好地檢查Kafka中Saga的執行進度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下展示了一個支付失敗的購買訂單的狀態轉換,首先訂單傳入,“credit-approval”步驟啓動:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n \"id\": \"73707ad2-0732-4592-b7e2-79b07c745e45\",\n \"currentstep\": null,\n \"payload\": \"\\\"order-id\\\": 2, \\\"customer-id\\\": 456, \\\"payment-due\\\": 4999, \\\"credit-card-no\\\": \\\"xxxx-yyyy-dddd-9999\\\"}\",\n \"sagastatus\": \"STARTED\",\n \"stepstatus\": \"{}\",\n \"type\": \"order-placement\",\n \"version\": 0\n}\n{\n \"id\": \"73707ad2-0732-4592-b7e2-79b07c745e45\",\n \"currentstep\": \"credit-approval\",\n \"payload\": \"{ \\\"order-id\\\": 2, \\\"customer-id\\\": 456, ... }\",\n \"sagastatus\": \"STARTED\",\n \"stepstatus\": \"{\\\"credit-approval\\\": \\\"STARTED\\\"}\",\n \"type\": \"order-placement\",\n \"version\": 1\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時,一條“credit-approval”請求消息也會持久化到發件箱表中。消息發送到Kafka之後,消費者服務將會處理它併發送一條答覆消息。訂單服務會更新Saga狀態並開始支付步驟:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n \"id\": \"73707ad2-0732-4592-b7e2-79b07c745e45\",\n \"currentstep\": \"payment\",\n \"payload\": \"{ \\\"order-id\\\": 2, \\\"customer-id\\\": 456, ... }\",\n \"sagastatus\": \"STARTED\",\n \"stepstatus\": \"{\\\"payment\\\": \\\"STARTED\\\", \\\"credit-approval\\\": \\\"SUCCEEDED\\\"}\",\n \"type\": \"order-placement\",\n \"version\": 2\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"消息會再次通過發件箱表進行發送,不過現在是“payment”請求。如果這個步驟失敗了,支付系統會發送一個答覆消息作爲響應,並表明發生了什麼情況。這也就意味着“credit-approval”步驟需要通過消費者系統進行補償:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n \"id\": \"73707ad2-0732-4592-b7e2-79b07c745e45\",\n \"currentstep\": \"credit-approval\",\n \"payload\": \"{ \\\"order-id\\\": 2, \\\"customer-id\\\": 456, ... }\",\n \"sagastatus\": \"ABORTING\",\n \"stepstatus\": \"{\\\"payment\\\": \\\"FAILED\\\", \\\"credit-approval\\\": \\\"COMPENSATING\\\"}\",\n \"type\": \"order-placement\",\n \"version\": 3\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個步驟完成後,Saga會進入最後的狀態,也就是"},{"type":"codeinline","content":[{"type":"text","text":"ABORTED"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n \"id\": \"73707ad2-0732-4592-b7e2-79b07c745e45\",\n \"currentstep\": null,\n \"payload\": \"{ \\\"order-id\\\": 2, \\\"customer-id\\\": 456, ... }\",\n \"sagastatus\": \"ABORTED\",\n \"stepstatus\": \"{\\\"payment\\\": \\\"FAILED\\\", \\\"credit-approval\\\": \\\"COMPENSATED\\\"}\",\n \"type\": \"order-placement\",\n \"version\": 4\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以按照樣例中README文件中的"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga#running-the-example","title":"","type":null},"content":[{"type":"text","text":"說明"}]},{"type":"text","text":"自行嘗試一下,在這裏你可以找到創建訂單"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/blob\/master\/saga\/requests\/place-order.json","title":"","type":null},"content":[{"type":"text","text":"成功"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/blob\/master\/saga\/requests\/place-invalid-order2.json","title":"","type":null},"content":[{"type":"text","text":"失敗"}]},{"type":"text","text":"的請求。這裏還包含如何檢查Kafka主題中交換消息的指南,這些消息都來自不同服務的發件箱表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我們看一下這個用例的部分具體實現。Saga流是在訂單服務中啓動的,其REST端點實現如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@POST\n@Transactional\npublic PlaceOrderResponse placeOrder(PlaceOrderRequest req) {\n PurchaseOrder order = req.toPurchaseOrder();\n order.persist(); \n\n sagaManager.begin(OrderPlacementSaga.class, OrderPlacementSaga.payloadFor(order)); \n\n return PlaceOrderResponse.fromPurchaseOrder(order);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"持久化傳入的購買訂單"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲傳入的訂單開啓下單的Saga流"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"SagaMananger.begin()"}]},{"type":"text","text":"會在"},{"type":"codeinline","content":[{"type":"text","text":"sagastate"}]},{"type":"text","text":"表中創建一條新的記錄,通過"},{"type":"codeinline","content":[{"type":"text","text":"OrderPlacementSaga"}]},{"type":"text","text":"實現獲取第一個發件箱事件並將其持久化到發件箱表中。"},{"type":"codeinline","content":[{"type":"text","text":"OrderPlacementSaga"}]},{"type":"text","text":"類要實現Saga流中與該用例相關的所有具體的組成部分,包括:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用來執行Saga流中某個組成部分的發件箱事件"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用來補償Saga流中某個組成部分的發件箱事件"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事件處理器,用來處理來自其他Saga參與者的答覆消息"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"OrderPlacementSaga"}]},{"type":"text","text":"實現太長了,並不適合在這裏全部展示(你可以在"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/blob\/master\/saga\/order-service\/src\/main\/java\/io\/debezium\/examples\/saga\/order\/saga\/OrderPlacementSaga.java","title":"","type":null},"content":[{"type":"text","text":"GitHub上查閱它的完整代碼"}]},{"type":"text","text":"),但是這裏我們展示了一些核心的組成部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@Saga(type=\"order-placement\", stepIds = {CREDIT_APPROVAL, PAYMENT}) 1️⃣\npublic class OrderPlacementSaga extends SagaBase {\n\n private static final String REQUEST = \"REQUEST\";\n private static final String CANCEL = \"CANCEL\";\n protected static final String PAYMENT = \"payment\";\n protected static final String CREDIT_APPROVAL = \"credit-approval\";\n\n \/\/ ...\n @Override\n public SagaStepMessage getStepMessage(String id) { 2️⃣\n if (id.equals(PAYMENT)) {\n return new SagaStepMessage(PAYMENT, REQUEST, getPayload());\n }\n else {\n return new SagaStepMessage(CREDIT_APPROVAL, REQUEST, getPayload());\n }\n }\n\n @Override\n public SagaStepMessage getCompensatingStepMessage(String id) { 3️⃣\n \/\/ ...\n }\n\n public void onPaymentEvent(PaymentEvent event) { 4️⃣\n if (alreadyProcessed(event.messageId)) {\n return;\n }\n\n onStepEvent(PAYMENT, event.status.toStepStatus());\n updateOrderStatus();\n\n processed(event.messageId);\n }\n\n public void onCreditApprovalEvent(CreditApprovalEvent event) { 5️⃣\n \/\/ ...\n }\n\n private void updateOrderStatus() { 6️⃣\n if (getStatus() == SagaStatus.COMPLETED) {\n PurchaseOrder order = PurchaseOrder.findById(getOrderId());\n order.status = PurchaseOrderStatus.ACCEPTED;\n }\n else if (getStatus() == SagaStatus.ABORTED) {\n PurchaseOrder order = PurchaseOrder.findById(getOrderId());\n order.status = PurchaseOrderStatus.CANCELLED;\n }\n }\n\n \/\/ ...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1️⃣ Saga步驟的id,便於執行"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2️⃣ 返回給定的步驟要發佈的發件箱消息"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3️⃣ 返回補償步驟要發佈的發件箱消息"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4️⃣ 針對“payment”答覆消息的事件處理器,它會更新購買訂單的狀態以及Saga的狀態(通過onStepEvent()回調實現),根據不同的狀態,它可能會完成Saga,也可能會通過應用所有的補償消息啓動它的回滾過程。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5️⃣ 針對“credit approval”答覆消息的事件處理器"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6️⃣ 基於當前的Saga狀態,更新購買訂單的狀態"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"...\n\nthis.outboxEvent.fire(CreditEvent.of(sagaId, CreditStatus.CANCELLED));\n...\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"消費者服務和支付服務並沒有什麼新鮮的內容,所以簡潔起見,我們在這裏就略過它們了。你可以在"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga\/customer-service","title":"","type":null},"content":[{"type":"text","text":"這裏"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/github.com\/debezium\/debezium-examples\/tree\/master\/saga\/payment-service","title":"","type":null},"content":[{"type":"text","text":"這裏"}]},{"type":"text","text":"查閱它們的完整代碼。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如果事情出錯會怎樣"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現像Saga這樣的分佈式交互模式時,一個關鍵的組成部分就是了解它們在出現故障時的表現,並確保在不可預見的情況下,也能實現(最終)一致性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,Saga步驟的負面輸出(比如,支付服務因爲無效的信用卡而拒絕支付)並不算是這裏所說的故障場景,因爲我們明確預期參與者可能無法執行整體流程中屬於它們的那一部分,從而會導致對應的補償本地事務會被執行。這意味着,這種通用的參與者執行故障不得引發本地數據庫事務的回滾,因爲否則的話,就不會有答覆消息通過發件箱發送給編排者了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"記住了這一點,我們就來討論一些可能的故障場景:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Kafka消息的事件處理器拋出了異常"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本地數據庫事務被回滾了,而消息消費者沒有向Kafka代理確認(acknowledge)它能夠處理消息。因爲代理沒有接收到消息已經得到處理的確認信息,所以在一定的時間之後,它就會重複性地重發該消息,直到得到確認爲止。我們應該有監控措施來探測這種場景,因爲在消息得到處理之前,Saga流不會繼續進行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Debezium connector在發送發件箱消息給Kafka之後就崩潰了,此時還沒有在源數據庫事務日誌中提交偏移(offset)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重啓connector之後,它將會繼續從上次提交日誌偏移的地方在發件箱表中讀取消息,這有可能造成有些發件箱事件會發送兩次,這也就是爲何要求所有參與者都是冪等的,就像前面的例子中通過使用唯一的消息id來實現的那樣,消費者還能通過journal表跟蹤成功處理過的消息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Kafka沒有運行或者無法訪問,例如由於網絡分割所致。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Debezium connector能夠在Kafka再次可用時恢復它們的工作,但是在此之前,Saga流自然無法進行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"消息已經得到了處理,但是向Kafka確認的時候失敗了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這條消息會再次傳遞給消費者服務,而在消費者服務的journal表中會找到該消息的id,因此會作爲重複消息被忽略掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"並行處理多個Saga步驟時,對Saga狀態表的併發更新"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然我們已經討論了編排者如何通過依次觸發參與服務形成順序化的流程,但是我們也應該設想一下並行處理多個步驟的Saga實現。在這種情況下,併發到達的答覆信息可能會競爭更新Saga的狀態表。這種場景會通過該表上的樂觀鎖探測到,會導致事件處理器試圖去提交更新給一條已經過時的Saga狀態版本,從而出現失敗、回滾和重試。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以討論更多的情況,但是總體設計的語義是最終一致的系統,能夠保證至少執行一次。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"額外的福利:分佈式跟蹤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在設計分佈式系統之間的事件流時,運維上的洞察力對於確保一切正確和高效運行至關重要。分佈式跟蹤提供了這樣的洞察力:它會收集每個系統的跟蹤信息,這些系統會貢獻這樣的交互信息,並且允許對調用流進行檢查,例如以Web UI的形式,這使得它成爲了故障分析和調試的寶貴工具。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Debezium的發件箱通過與"},{"type":"link","attrs":{"href":"https:\/\/opentracing.io\/","title":"","type":null},"content":[{"type":"text","text":"OpenTracing"}]},{"type":"text","text":"(對"},{"type":"link","attrs":{"href":"https:\/\/opentelemetry.io\/","title":"","type":null},"content":[{"type":"text","text":"OpenTelemetry"}]},{"type":"text","text":" 的支持已經在路線圖上了)規範的緊密結合解決了這個問題。通過"},{"type":"link","attrs":{"href":"https:\/\/jaegertracing.io\/","title":"","type":null},"content":[{"type":"text","text":"Jaeger"}]},{"type":"text","text":"這樣的工具,只需要一些"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/documentation\/reference\/integrations\/tracing.html","title":"","type":null},"content":[{"type":"text","text":"配置"}]},{"type":"text","text":",就能收集訂單、消費者和支付服務的跟蹤信息,並將它們展現爲端到端的跟蹤結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/44\/06\/4471819160de1a5f731c41e824ea2f06.jpg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"圖7:Saga流上的Jaeger UI"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Jaeger中的可視化很好地爲我們展示了Saga流是如何通過訂單服務中的傳入REST請求(1)觸發的,發件箱消息發送給消費者服務(2)並傳送回訂單服務(3),隨後另外一條消息發送給支付服務(4)並最終再次發送回訂單服務(5)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助跟蹤功能,我們能夠很容易地識別未完成的流(例如,因爲某個參與服務的事件處理器未能成功處理某條消息)和性能瓶頸(例如,某個事件處理器需要一個不合理的時間才能完成Saga流中屬於自己的那一部分)。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結與展望"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Saga模式爲實現長時間運行的”業務事務“提供了一個強大而靈活的解決方案,這需要多個獨立的服務就應用還是放棄一組數據變更達成一致。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助CDC、Debezium和Apache Kafka實現的發送者模式,Saga編排者能夠與所有參與服務的可用性解耦。單個參與服務的臨時中斷不會影響整體的Saga流:組件恢復之後,Saga將會從之前中斷的地方繼續進行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,我們應該期望服務之間是互相分割的,儘可能減少與遠程服務之間互動的需求。例如,把信用額度的邏輯轉移到訂單服務本身之中,避免與消費者服務的協同,這可能也是一個可選方案。但是,根據業務的需要,這種跨多個服務的交互可能是難以避免的,特別是涉及到集成遺留系統,或者系統不在我們的控制之中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現像Saga這樣的複雜模式時,準確理解它們的約束和語義是至關重要的。在我們建議的解決方案背景中,有兩件事需要注意,那就是固有的最終一致性以及總體業務事務的有限隔離級別。例如,因爲一個訂單給消費者分配了部分信用額度可能會導致同時提交的另外一個訂單被拒絕,而第一個訂單最終卻可能並沒有真正完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文討論的樣例項目基於CDC和發件箱模式提供了一個概念驗證級別的Saga編排實現,它被組織成了兩部分:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個通用的“框架”組件,它以簡單狀態機的形式提供了Saga編排的邏輯,同時提供了Saga執行日誌。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們所討論的下訂單的具體實現(上文所示的"},{"type":"codeinline","content":[{"type":"text","text":"OrderPlacementSaga"}]},{"type":"text","text":"類以及相關的REST端點等)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"更進一步的話,我們可能會將前一部分提取成一個可重用的組件,比如通過現有的Debezium Quarkus擴展實現。如果你對此感興趣的話,可以通過Debezium的"},{"type":"link","attrs":{"href":"https:\/\/groups.google.com\/g\/debezium","title":"","type":null},"content":[{"type":"text","text":"郵件列表"}]},{"type":"text","text":"聯繫我們。一個可能增加的功能是併發執行多個Saga步驟的方法。這是否合理是一個商業決定,但從技術角度來看,支持它並不難。在這種情況下,更新Saga狀態時的競爭可能會成爲一個關鍵問題,"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/particular.net\/blog\/optimizations-to-scatter-gather-sagas","title":"","type":null},"content":[{"type":"text","text":"分散-聚集Saga的優化"}]},{"type":"text","text":"一文討論了在這方面可能的解決方案。如果能有一個設施來監控和識別那些在一段時間後還沒有完成的Saga,也是很有意思的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們所提議的實現提供了一種可靠執行業務的方式,能夠在跨多個服務時實現”全有或全無“的語義。對於有更復雜需求的用例,比如帶有條件邏輯的流程,那麼就可以瞭解一下現有的工作流引擎和業務處理自動化工具,比如"},{"type":"link","attrs":{"href":"https:\/\/kogito.kie.org\/","title":"","type":null},"content":[{"type":"text","text":"Kogito"}]},{"type":"text","text":"。另一項值得關注的技術是針對"},{"type":"link","attrs":{"href":"https:\/\/github.com\/eclipse\/microprofile-lra","title":"","type":null},"content":[{"type":"text","text":"長時間運行的活動(long-running activities,LRA)的MicroProfile規範"}]},{"type":"text","text":",該規範目前正在開發中。MicroProfile社區也在討論"},{"type":"link","attrs":{"href":"https:\/\/github.com\/eclipse\/microprofile-lra\/issues\/338","title":"","type":null},"content":[{"type":"text","text":"與Debezium這樣的事務性發件箱實現的整合"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"非常感謝"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/hpgrahsl","title":"","type":null},"content":[{"type":"text","text":"Hans-Peter Grahsl"}]},{"type":"text","text":"、"},{"type":"link","attrs":{"href":"https:\/\/github.com\/roldanbob","title":"","type":null},"content":[{"type":"text","text":"Bob Roldan"}]},{"type":"text","text":"、"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/nmcl","title":"","type":null},"content":[{"type":"text","text":"Mark Little"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/ThomasBetts","title":"","type":null},"content":[{"type":"text","text":"Thomas Betts"}]},{"type":"text","text":"在寫這篇文章時提供的大量反饋!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查看英文原文:"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/saga-orchestration-outbox\/","title":"","type":null},"content":[{"type":"text","text":"Saga Orchestration for Microservices Using the Outbox Pattern"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Gunnar Morling是一名軟件工程師,熱情的開源愛好者。他正在領導"},{"type":"link","attrs":{"href":"https:\/\/debezium.io\/","title":"","type":null},"content":[{"type":"text","text":"Debezium"}]},{"type":"text","text":"項目,這是一個用於變更數據捕獲(CDC)的工具。他是一名Java Champion,是"},{"type":"link","attrs":{"href":"https:\/\/beanvalidation.org\/2.0\/","title":"","type":null},"content":[{"type":"text","text":"Bean Validation 2.0(JSR 380)"}]},{"type":"text","text":"的規範負責人,並創立了多個開源項目,如"},{"type":"link","attrs":{"href":"https:\/\/github.com\/moditect\/layrry","title":"","type":null},"content":[{"type":"text","text":"Layrry"}]},{"type":"text","text":"、"},{"type":"link","attrs":{"href":"https:\/\/github.com\/moditect\/deptective","title":"","type":null},"content":[{"type":"text","text":"Deptective"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/mapstruct.org\/","title":"","type":null},"content":[{"type":"text","text":"MapStruct"}]},{"type":"text","text":"。在加入Red Hat之前,Gunnar廣泛在物流和零售行業從事Java EE相關的項目。他的工作地點在德國漢堡。你可以通過推特聯繫到他:"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/gunnarmorling","title":"","type":null},"content":[{"type":"text","text":"@gunnarmorling"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接"},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/www.zybuluo.com\/levinzhang\/note\/1790212","title":"","type":null},"content":[{"type":"text","text":"使用發件箱模式實現微服務的Saga編排"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章