狀態機在馬蜂窩機票訂單交易系統中的應用與優化實踐

在設計交易系統時,穩定性、可擴展性、可維護性都是咱們須要關注的重點。本文將對如何經過狀態機在交易系統中的應用解決上述問題作出一些探討。數據庫

關於馬蜂窩機票訂單交易系統

交易系統每每存在訂單維度多、狀態多、交易鏈路長、流程複雜等特色。以馬蜂窩大交通業務中的機票交易爲例,用戶提交的一個訂單除了機票信息以外可能還包含不少信息,好比保險或者其餘附加產品。其中保險又分爲不少類型,如航意險、航延險、組合險等。併發

從用戶的維度看,一個訂單是由購買的主產品機票和附加產品共同構成,支付的時候是做爲一個總體去支付,而若是想要退票、退保也是能夠部分操做的;從供應商的維度看,一個訂單中的每一個產品背後都有獨立的供應商,機票有機票的供應商,保險有保險的供應商,每一個供應商的訂單都須要分開出票、獨立結算。框架

用戶的購買支付流程、供應商的出票出保流程,構成一個有機的總體穿插在機票交易系統中,密不可分。less

狀態機在機票交易系統中的應用與優化

有限狀態機的概念

有限狀態機(如下簡稱狀態機)是一種用於對事物或者對象行爲進行建模的工具。異步

狀態機將複雜的邏輯簡化爲有限個穩定狀態,構建在這些狀態之間的轉移和動做等行爲的數學模型,在穩定狀態中判斷事件。數據庫設計

對狀態機輸入一個事件,狀態機會根據當前狀態和觸發的事件惟一肯定一個狀態遷移。工具

圖1:FSM工做原理優化

業務系統的本質就是描述真實的世界,所以幾乎全部的業務系統中都會有狀態機的影子。訂單交易流程更是自然適合狀態機模型的應用。ui

以用戶支付流程爲例,若是不使用狀態機,在接收到支付成功回調時則須要執行一系列動做:查詢支付流水號、記錄支付時間、修改主訂單狀態爲已支付、通知供應商去出票、記錄通知出票時間、修改機票子訂單狀態爲出票中…… 邏輯很是繁瑣,並且代碼耦合嚴重。spa

爲了使交易系統的訂單狀態按照設計流程正確向下流轉,好比當前用戶已支付,不容許再支付;當前訂單已經關單,不能再通知出票等等,咱們經過應用狀態機的方式來優化機票交易系統,將全部的狀態、事件、動做都抽離出來,對複雜的狀態遷移邏輯進行統一管理,來取代冗長的 if else 判斷,使機票交易系統中的複雜問題得以解耦,變得直觀、方便操做,使系統更加易於維護和管理。

狀態機設計

在數據庫設計層面,咱們將整個訂單總體做爲一個主訂單,把供應商的訂單做爲子訂單。假設一個用戶同時購買了機票和保險,由於機票、保險對應的是不一樣的供應商,也就是 1 個主訂單  order  對應 2 個子訂單 sub_order。其中主訂單 order 記錄用戶的信息(UID、聯繫方式、訂單總價格等),子訂單 sub_order 記錄產品類型、供應商訂單號、結算價格等。

同時,咱們把正向出票、逆向退票改簽分開,抽成不一樣的子系統。這樣每一個子系統都是徹底獨立的,有利於系統的維護和拓展。

對於機票正向子系統而言,有兩套狀態機:主訂單狀態機負責管理 order 的狀態,包括創單成功、支付成功、交易成功、訂單關閉等;子訂單狀態機負責管理 sub_order 的狀態,維護預訂成功到出票的流程。一樣,對於逆向退票和改簽子系統,也會有各自的狀態機。

圖2:機票主訂單狀態機狀態轉移示例

框架選型

目前業界經常使用的狀態機引擎框架主要有 Spring Statemachine、Stateless4j、Squirrel-Foundation 等。通過結合實際業務進行橫向對比後,最終咱們決定使用 Squirrel-Foundation,主要是由於:

  1. 代碼量適中,擴展和維護相對而言比較容易;
  2. StateMachine 輕量,實例建立開銷小;
  3. 切入點豐富,支持狀態進入、狀態完成、異常等節點的監聽,使轉換過程留有足夠的切入點;
  4. 支持使用註解定義狀態轉移,使用方便;
  5. 從設計上不支持單例複用,只能隨用隨 New,所以狀態機的自己的生命流管理很清晰,不會由於狀態機單例複用的問題形成麻煩。 

MSM 的設計與實現

結合大交通業務邏輯,咱們在 Squirrel-Foundation 的基礎之上進行了 Action 概念的抽取和二次封裝,將狀態遷移、異步消息糅合到一塊兒,封裝成爲 MSM 框架 (MFW State Machine),用來實現業務訂單狀態定義、事件定義和狀態機定義,並用註解的形式來描述狀態遷移。

咱們認爲一次狀態遷移必然會伴隨着異步消息,所以把一個流程中必需要成功的數據庫操做放到一個事務中,把容許失敗重試而且對實時度要求不高的操做放到異步消息消費的流程中。

以機票訂單支付成功爲例,機票訂單支付成功時,會涉及修改訂單狀態爲已支付、更新支付流水號等,這些是在一個事務中;而通知供應商出票,則是放在異步消息消費中處理。異步消息的實現使用的是 RocketMQ,主要考慮到 RocketMQ 支持二階段提交,消息可靠性有保證,支持重試,支持多個 Consumer 組。

如下具體說明:

1. 對每一個狀態遷移須要執行的動做,都會抽取出一個Action 類,而且繼承 AbstractAction,支持多個不一樣的狀態遷移執行相同的動做。這裏主要取決於 public List<ActionCondition> matchConditions() 的實現,所以只須要 matchConditions 返回多個初始狀態-事件的匹配條件鍵值對就能夠了。每一個 Action 都有一個對應的繼承 MFWContext 類的上下文類,用於在 process saveDB 等方法中的通訊。

2. 註冊全部的 Action,添加每一個狀態遷移執行完成或者執行失敗的監聽。

3. 因爲依賴 RocketMQ 異步消息,因此須要一個 Spring Bean 去繼承 BaseMessageSender,這個類會生成異步消息提供者。若是要使用二階段提交,則須要一個類繼承 BaseMsgTransactionListener,這裏能夠參考機票的 OrderChangeMessageSender 和 OrderChangeMsgTransactionListener。

4. 最後,實現一個事件觸發器類。在這個類裏面包含一個 Apply 方法,傳入訂單 PO 對象、事件、對應的上下文,每次執行都實例化出一個狀態機實例,並初始化當前狀態,並調用 Fire 方法。

5. 實例化一個狀態機對象,設置當前狀態爲數據庫對應的狀態,調用 Fire 方法以後,最終會執行到 OrderStateMachine 類裏面用註解描述的 callMethod 方法。咱們配置的是 callMethod = "action",它就會反射執行當前類的 Action 方法。

Action 方法咱們的實現是經過 super.action(from, to, event, context),就會執行 MFWStateMachine 的 Action 方法,先去根據當前狀態和事件獲取對應的Action,這裏使用到了「工廠模式」,而後執行 Process 方法。若是成功,會執行在 MFWStateMachine 類初始化的 TransitionCompleteListener,執行該 Action的 afterProcess 方法來修改數據庫記錄以及發送消息;若是失敗,會執行TransitionExceptionListener,執行該 Action 的onException 方法來進行相應處理。

綜上,MSM 能夠根據 Action 類的聲明和配置,來動態生成出 Squirrel-Foundation 的狀態機定義,而不須要由使用方再去定義一次,使 MSM 的使用更方便。

圖3: UML

趟過的坑

1. 事務不生效

最初咱們使用 Spring 註解方式進行事務管理,即在 Action 類的數據庫操做方法上加 @Transactional 註解,卻發如今實踐中不起做用。通過排查後發現, Spring 的事務註解是靠 AOP 切面實現的。在對象內部的方法中調用該對象其餘使用 AOP 註解的方法,被調用方法的 AOP 註解會失效。由於同一個類的內部代碼調用中,不會走代理類。後來咱們經過手動開啓事務的方式來解決此問題。

2. 匹配 Action  

最初咱們匹配 Action 有兩種方式:精準匹配及非精準匹配。精準匹配是指只有當某個狀態遷移的初始狀態和觸發的事件一致時,才能匹配到 Action;非精準匹配是指只要觸發的事件一致,就能夠匹配到 Action。後來咱們發現非精準匹配在某些情形下會出現問題,因而統一改爲了多條件精準匹配。即在執行狀態機觸發時執行的 Action 方法時,去精準匹配 Action,多個狀態遷移執行的方法能夠匹配到同一個 Action,這樣可以複用 Action 代碼而不會出問題。 

3. 異步消息一致性 

有一些狀況是毫不能出現的,好比修改數據庫沒成功即發出了消息;或是修改數據庫成功了,而發送消息失敗;或是在提交數據庫事務以前,消息已經發送成功了。解決這個問題咱們用到了 RocketMQ 的事務消息功能,它支持二階段提交,會先發送一條預處理消息,而後回調執行本地事務,最終提交或者回滾,幫助保證修改數據庫的信息和發送異步消息的一致。

4. 同一條訂單數據併發執行不一樣事件 

在某些狀況下,同一條訂單數據可能會在同一時間(毫秒級)同時觸發不一樣的事件。如機票主訂單在待支付狀態下,能夠接收支付中心的回調,觸發支付成功事件;也能夠由用戶點擊取消訂單,或者超時未支付定時任務來觸發關單事件。若是不作任何控制的話,一個訂單將可能出現既支付成功又會被取消。

咱們用數據庫樂觀鎖來規避這個問題:在執行修改數據庫的事務時,update 訂單的語句帶有原狀態的條件判斷,經過判斷更新行數是否爲 1,來決定是否拋出異常,即生成這樣的 SQL 語句:update order where order_id = ‘1234' and order_status = ‘待支付'。

這樣的話,若是兩個事件同時觸發同時執行,誰先把事務提交成功,誰就能執行成功;事務提交較晚的事件會由於更新行數爲 0 而執行失敗,最終回滾事務,就彷彿無事發生過同樣。

使用悲觀鎖也能夠解決這個問題,這種方式是誰先爭搶到鎖誰就能夠成功執行。但考慮到可能會有腳本對數據庫批量修改,悲觀鎖存在死鎖的潛在問題,咱們最終仍是採用了樂觀鎖的方式。

總結

MSM 狀態機的定義和聲明在 Squirrel-Foundation 的基礎之上,抽取出 Action 概念,並對 Action 類配置起始狀態、目標狀態、觸發的事件、上下文定義等,使 MSM 能夠根據 Action 類的聲明和配置,來動態生成出 Squirrel-Foundation 的狀態機定義,而不須要使用方再去定義一次,操做更簡單,維護起來也更容易。 

經過使用狀態機,機票訂單交易系統的流程複雜性問題迎刃而解,系統在穩定性、可擴展性、可維護性等方面也獲得了顯著的改善和提高。

狀態機的使用場景不只僅侷限於訂單交易系統,其餘一些涉及到狀態變動的複雜流程的系統也一樣適用。但願經過本文的介紹,能使有狀態機瞭解和使用需求的讀者朋友有所收穫。

本文做者:董天,馬蜂窩大交通研發團隊機票交易系統研發工程師。

(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,謝謝配合。)

關注馬蜂窩技術,找到更多你想要的內容

相關文章
相關標籤/搜索