狀態(tài)機(jī)在馬蜂窩機(jī)票訂單交易系統(tǒng)中的應(yīng)用與優(yōu)化實(shí)踐
在設(shè)計(jì)交易系統(tǒng)時(shí),穩(wěn)定性、可擴(kuò)展性、可維護(hù)性都是我們需要關(guān)注的重點(diǎn)。本文將對(duì)如何通過狀態(tài)機(jī)在交易系統(tǒng)中的應(yīng)用解決上述問題做出一些探討。
關(guān)于馬蜂窩機(jī)票訂單交易系統(tǒng)
交易系統(tǒng)往往存在訂單維度多、狀態(tài)多、交易鏈路長(zhǎng)、流程復(fù)雜等特點(diǎn)。以馬蜂窩大交通業(yè)務(wù)中的機(jī)票交易為例,用戶提交的一個(gè)訂單除了機(jī)票信息之外可能還包含很多信息,比如保險(xiǎn)或者其他附加產(chǎn)品。其中保險(xiǎn)又分為很多類型,如航意險(xiǎn)、航延險(xiǎn)、組合險(xiǎn)等。
從用戶的維度看,一個(gè)訂單是由購買的主產(chǎn)品機(jī)票和附加產(chǎn)品共同構(gòu)成,支付的時(shí)候是作為一個(gè)整體去支付,而如果想要退票、退保也是可以部分操作的;從供應(yīng)商的維度看,一個(gè)訂單中的每個(gè)產(chǎn)品背后都有獨(dú)立的供應(yīng)商,機(jī)票有機(jī)票的供應(yīng)商,保險(xiǎn)有保險(xiǎn)的供應(yīng)商,每個(gè)供應(yīng)商的訂單都需要分開出票、獨(dú)立結(jié)算。
用戶的購買支付流程、供應(yīng)商的出票出保流程,構(gòu)成一個(gè)有機(jī)的整體穿插在機(jī)票交易系統(tǒng)中,密不可分。
狀態(tài)機(jī)在機(jī)票交易系統(tǒng)中的應(yīng)用與優(yōu)化
有限狀態(tài)機(jī)的概念
有限狀態(tài)機(jī)(以下簡(jiǎn)稱狀態(tài)機(jī))是一種用于對(duì)事物或者對(duì)象行為進(jìn)行建模的工具。
狀態(tài)機(jī)將復(fù)雜的邏輯簡(jiǎn)化為有限個(gè)穩(wěn)定狀態(tài),構(gòu)建在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)模型,在穩(wěn)定狀態(tài)中判斷事件。
對(duì)狀態(tài)機(jī)輸入一個(gè)事件,狀態(tài)機(jī)會(huì)根據(jù)當(dāng)前狀態(tài)和觸發(fā)的事件唯一確定一個(gè)狀態(tài)遷移。
圖1:FSM工作原理
業(yè)務(wù)系統(tǒng)的本質(zhì)就是描述真實(shí)的世界,因此幾乎所有的業(yè)務(wù)系統(tǒng)中都會(huì)有狀態(tài)機(jī)的影子。訂單交易流程更是天然適合狀態(tài)機(jī)模型的應(yīng)用。
以用戶支付流程為例,如果不使用狀態(tài)機(jī),在接收到支付成功回調(diào)時(shí)則需要執(zhí)行一系列動(dòng)作:查詢支付流水號(hào)、記錄支付時(shí)間、修改主訂單狀態(tài)為已支付、通知供應(yīng)商去出票、記錄通知出票時(shí)間、修改機(jī)票子訂單狀態(tài)為出票中…… 邏輯非常繁瑣,而且代碼耦合嚴(yán)重。
為了使交易系統(tǒng)的訂單狀態(tài)按照設(shè)計(jì)流程正確向下流轉(zhuǎn),比如當(dāng)前用戶已支付,不允許再支付;當(dāng)前訂單已經(jīng)關(guān)單,不能再通知出票等等,我們通過應(yīng)用狀態(tài)機(jī)的方式來優(yōu)化機(jī)票交易系統(tǒng),將所有的狀態(tài)、事件、動(dòng)作都抽離出來,對(duì)復(fù)雜的狀態(tài)遷移邏輯進(jìn)行統(tǒng)一管理,來取代冗長(zhǎng)的 if else 判斷,使機(jī)票交易系統(tǒng)中的復(fù)雜問題得以解耦,變得直觀、方便操作,使系統(tǒng)更加易于維護(hù)和管理。
狀態(tài)機(jī)設(shè)計(jì)
在數(shù)據(jù)庫設(shè)計(jì)層面,我們將整個(gè)訂單整體作為一個(gè)主訂單,把供應(yīng)商的訂單作為子訂單。假設(shè)一個(gè)用戶同時(shí)購買了機(jī)票和保險(xiǎn),因?yàn)闄C(jī)票、保險(xiǎn)對(duì)應(yīng)的是不同的供應(yīng)商,也就是 1 個(gè)主訂單 order 對(duì)應(yīng) 2 個(gè)子訂單 sub_order。其中主訂單 order 記錄用戶的信息(UID、聯(lián)系方式、訂單總價(jià)格等),子訂單 sub_order 記錄產(chǎn)品類型、供應(yīng)商訂單號(hào)、結(jié)算價(jià)格等。
同時(shí),我們把正向出票、逆向退票改簽分開,抽成不同的子系統(tǒng)。這樣每個(gè)子系統(tǒng)都是完全獨(dú)立的,有利于系統(tǒng)的維護(hù)和拓展。
對(duì)于機(jī)票正向子系統(tǒng)而言,有兩套狀態(tài)機(jī):主訂單狀態(tài)機(jī)負(fù)責(zé)管理 order 的狀態(tài),包括創(chuàng)單成功、支付成功、交易成功、訂單關(guān)閉等;子訂單狀態(tài)機(jī)負(fù)責(zé)管理 sub_order 的狀態(tài),維護(hù)預(yù)訂成功到出票的流程。同樣,對(duì)于逆向退票和改簽子系統(tǒng),也會(huì)有各自的狀態(tài)機(jī)。
圖2:機(jī)票主訂單狀態(tài)機(jī)狀態(tài)轉(zhuǎn)移示例
框架選型
目前業(yè)界常用的狀態(tài)機(jī)引擎框架主要有 Spring Statemachine、Stateless4j、Squirrel-Foundation 等。經(jīng)過結(jié)合實(shí)際業(yè)務(wù)進(jìn)行橫向?qū)Ρ群?,最終我們決定使用 Squirrel-Foundation,主要是因?yàn)椋?/p>
- 代碼量適中,擴(kuò)展和維護(hù)相對(duì)而言比較容易;
- StateMachine 輕量,實(shí)例創(chuàng)建開銷小;
- 切入點(diǎn)豐富,支持狀態(tài)進(jìn)入、狀態(tài)完成、異常等節(jié)點(diǎn)的監(jiān)聽,使轉(zhuǎn)換過程留有足夠的切入點(diǎn);
- 支持使用注解定義狀態(tài)轉(zhuǎn)移,使用方便;
- 從設(shè)計(jì)上不支持單例復(fù)用,只能隨用隨 New,因此狀態(tài)機(jī)的本身的生命流管理很清晰,不會(huì)因?yàn)闋顟B(tài)機(jī)單例復(fù)用的問題造成麻煩。
MSM 的設(shè)計(jì)與實(shí)現(xiàn)
結(jié)合大交通業(yè)務(wù)邏輯,我們?cè)?Squirrel-Foundation 的基礎(chǔ)之上進(jìn)行了 Action 概念的抽取和二次封裝,將狀態(tài)遷移、異步消息糅合到一起,封裝成為 MSM 框架 (MFW State Machine),用來實(shí)現(xiàn)業(yè)務(wù)訂單狀態(tài)定義、事件定義和狀態(tài)機(jī)定義,并用注解的形式來描述狀態(tài)遷移。
我們認(rèn)為一次狀態(tài)遷移必然會(huì)伴隨著異步消息,因此把一個(gè)流程中必須要成功的數(shù)據(jù)庫操作放到一個(gè)事務(wù)中,把允許失敗重試并且對(duì)實(shí)時(shí)度要求不高的操作放到異步消息消費(fèi)的流程中。
以機(jī)票訂單支付成功為例,機(jī)票訂單支付成功時(shí),會(huì)涉及修改訂單狀態(tài)為已支付、更新支付流水號(hào)等,這些是在一個(gè)事務(wù)中;而通知供應(yīng)商出票,則是放在異步消息消費(fèi)中處理。異步消息的實(shí)現(xiàn)使用的是 RocketMQ,主要考慮到 RocketMQ 支持二階段提交,消息可靠性有保證,支持重試,支持多個(gè) Consumer 組。
以下具體說明:
1. 對(duì)每個(gè)狀態(tài)遷移需要執(zhí)行的動(dòng)作,都會(huì)抽取出一個(gè)Action 類,并且繼承 AbstractAction,支持多個(gè)不同的狀態(tài)遷移執(zhí)行相同的動(dòng)作。這里主要取決于 public List
2. 注冊(cè)所有的 Action,添加每個(gè)狀態(tài)遷移執(zhí)行完成或者執(zhí)行失敗的監(jiān)聽。
3. 由于依賴 RocketMQ 異步消息,所以需要一個(gè) Spring Bean 去繼承 BaseMessageSender,這個(gè)類會(huì)生成異步消息提供者。如果要使用二階段提交,則需要一個(gè)類繼承 BaseMsgTransactionListener,這里可以參考機(jī)票的 OrderChangeMessageSender 和 OrderChangeMsgTransactionListener。
4. 最后,實(shí)現(xiàn)一個(gè)事件觸發(fā)器類。在這個(gè)類里面包含一個(gè) Apply 方法,傳入訂單 PO 對(duì)象、事件、對(duì)應(yīng)的上下文,每次執(zhí)行都實(shí)例化出一個(gè)狀態(tài)機(jī)實(shí)例,并初始化當(dāng)前狀態(tài),并調(diào)用 Fire 方法。
5. 實(shí)例化一個(gè)狀態(tài)機(jī)對(duì)象,設(shè)置當(dāng)前狀態(tài)為數(shù)據(jù)庫對(duì)應(yīng)的狀態(tài),調(diào)用 Fire 方法之后,最終會(huì)執(zhí)行到 OrderStateMachine 類里面用注解描述的 callMethod 方法。我們配置的是 callMethod = "action",它就會(huì)反射執(zhí)行當(dāng)前類的 Action 方法。
Action 方法我們的實(shí)現(xiàn)是通過 super.action(from, to, event, context),就會(huì)執(zhí)行 MFWStateMachine 的 Action 方法,先去根據(jù)當(dāng)前狀態(tài)和事件獲取對(duì)應(yīng)的Action,這里使用到了「工廠模式」,然后執(zhí)行 Process 方法。如果成功,會(huì)執(zhí)行在 MFWStateMachine 類初始化的 TransitionCompleteListener,執(zhí)行該 Action的 afterProcess 方法來修改數(shù)據(jù)庫記錄以及發(fā)送消息;如果失敗,會(huì)執(zhí)行TransitionExceptionListener,執(zhí)行該 Action 的onException 方法來進(jìn)行相應(yīng)處理。
綜上,MSM 可以根據(jù) Action 類的聲明和配置,來動(dòng)態(tài)生成出 Squirrel-Foundation 的狀態(tài)機(jī)定義,而不需要由使用方再去定義一次,使 MSM 的使用更方便。
圖3: UML
趟過的坑
1. 事務(wù)不生效
最初我們使用 Spring 注解方式進(jìn)行事務(wù)管理,即在 Action 類的數(shù)據(jù)庫操作方法上加 @Transactional 注解,卻發(fā)現(xiàn)在實(shí)踐中不起作用。經(jīng)過排查后發(fā)現(xiàn), Spring 的事務(wù)注解是靠 AOP 切面實(shí)現(xiàn)的。在對(duì)象內(nèi)部的方法中調(diào)用該對(duì)象其他使用 AOP 注解的方法,被調(diào)用方法的 AOP 注解會(huì)失效。因?yàn)橥粋€(gè)類的內(nèi)部代碼調(diào)用中,不會(huì)走代理類。后來我們通過手動(dòng)開啟事務(wù)的方式來解決此問題。
2. 匹配 Action
最初我們匹配 Action 有兩種方式:精準(zhǔn)匹配及非精準(zhǔn)匹配。精準(zhǔn)匹配是指只有當(dāng)某個(gè)狀態(tài)遷移的初始狀態(tài)和觸發(fā)的事件一致時(shí),才能匹配到 Action;非精準(zhǔn)匹配是指只要觸發(fā)的事件一致,就可以匹配到 Action。后來我們發(fā)現(xiàn)非精準(zhǔn)匹配在某些情形下會(huì)出現(xiàn)問題,于是統(tǒng)一改成了多條件精準(zhǔn)匹配。即在執(zhí)行狀態(tài)機(jī)觸發(fā)時(shí)執(zhí)行的 Action 方法時(shí),去精準(zhǔn)匹配 Action,多個(gè)狀態(tài)遷移執(zhí)行的方法可以匹配到同一個(gè) Action,這樣能夠復(fù)用 Action 代碼而不會(huì)出問題。
3. 異步消息一致性
有一些情況是絕不能出現(xiàn)的,比如修改數(shù)據(jù)庫沒成功即發(fā)出了消息;或是修改數(shù)據(jù)庫成功了,而發(fā)送消息失敗;或是在提交數(shù)據(jù)庫事務(wù)之前,消息已經(jīng)發(fā)送成功了。解決這個(gè)問題我們用到了 RocketMQ 的事務(wù)消息功能,它支持二階段提交,會(huì)先發(fā)送一條預(yù)處理消息,然后回調(diào)執(zhí)行本地事務(wù),最終提交或者回滾,幫助保證修改數(shù)據(jù)庫的信息和發(fā)送異步消息的一致。
4. 同一條訂單數(shù)據(jù)并發(fā)執(zhí)行不同事件
在某些情況下,同一條訂單數(shù)據(jù)可能會(huì)在同一時(shí)間(毫秒級(jí))同時(shí)觸發(fā)不同的事件。如機(jī)票主訂單在待支付狀態(tài)下,可以接收支付中心的回調(diào),觸發(fā)支付成功事件;也可以由用戶點(diǎn)擊取消訂單,或者超時(shí)未支付定時(shí)任務(wù)來觸發(fā)關(guān)單事件。如果不做任何控制的話,一個(gè)訂單將可能出現(xiàn)既支付成功又會(huì)被取消。
我們用數(shù)據(jù)庫樂觀鎖來規(guī)避這個(gè)問題:在執(zhí)行修改數(shù)據(jù)庫的事務(wù)時(shí),update 訂單的語句帶有原狀態(tài)的條件判斷,通過判斷更新行數(shù)是否為 1,來決定是否拋出異常,即生成這樣的 SQL 語句:update order where order_id = ‘1234' and order_status = ‘待支付'。
這樣的話,如果兩個(gè)事件同時(shí)觸發(fā)同時(shí)執(zhí)行,誰先把事務(wù)提交成功,誰就能執(zhí)行成功;事務(wù)提交較晚的事件會(huì)因?yàn)楦滦袛?shù)為 0 而執(zhí)行失敗,最終回滾事務(wù),就仿佛無事發(fā)生過一樣。
使用悲觀鎖也可以解決這個(gè)問題,這種方式是誰先爭(zhēng)搶到鎖誰就可以成功執(zhí)行。但考慮到可能會(huì)有腳本對(duì)數(shù)據(jù)庫批量修改,悲觀鎖存在死鎖的潛在問題,我們最終還是采用了樂觀鎖的方式。
總結(jié)
MSM 狀態(tài)機(jī)的定義和聲明在 Squirrel-Foundation 的基礎(chǔ)之上,抽取出 Action 概念,并對(duì) Action 類配置起始狀態(tài)、目標(biāo)狀態(tài)、觸發(fā)的事件、上下文定義等,使 MSM 可以根據(jù) Action 類的聲明和配置,來動(dòng)態(tài)生成出 Squirrel-Foundation 的狀態(tài)機(jī)定義,而不需要使用方再去定義一次,操作更簡(jiǎn)單,維護(hù)起來也更容易。
通過使用狀態(tài)機(jī),機(jī)票訂單交易系統(tǒng)的流程復(fù)雜性問題迎刃而解,系統(tǒng)在穩(wěn)定性、可擴(kuò)展性、可維護(hù)性等方面也得到了顯著的改善和提升。
狀態(tài)機(jī)的使用場(chǎng)景不僅僅局限于訂單交易系統(tǒng),其他一些涉及到狀態(tài)變更的復(fù)雜流程的系統(tǒng)也同樣適用。希望通過本文的介紹,能使有狀態(tài)機(jī)了解和使用需求的讀者朋友有所收獲。
本文作者:董天,馬蜂窩大交通研發(fā)團(tuán)隊(duì)機(jī)票交易系統(tǒng)研發(fā)工程師。
【本文是51CTO專欄作者馬蜂窩技術(shù)的原創(chuàng)文章,作者微信公眾號(hào)馬蜂窩技術(shù)(ID:mfwtech)】