管理訂單狀態,該上狀態機嗎?輕量級狀態機COLA StateMachine保姆級入門教程

来源:https://www.cnblogs.com/rude3knife/archive/2022/06/02/cola-statemachine.html
-Advertisement-
Play Games

前言 在平常的後端項目開發中,狀態機模式的使用其實沒有大家想象中那麼常見,筆者之前由於不在電商領域工作,很少在業務代碼中用狀態機來管理各種狀態,一般都是手動get/set狀態值。去年筆者進入了電商領域從事後端開發。電商領域,狀態又多又複雜,如果仍然在業務代碼中東一塊西一塊維護狀態值,很容易陷入出了問 ...


前言

在平常的後端項目開發中,狀態機模式的使用其實沒有大家想象中那麼常見,筆者之前由於不在電商領域工作,很少在業務代碼中用狀態機來管理各種狀態,一般都是手動get/set狀態值。去年筆者進入了電商領域從事後端開發。電商領域,狀態又多又複雜,如果仍然在業務代碼中東一塊西一塊維護狀態值,很容易陷入出了問題難於Debug,難於追責的窘境。

碰巧有個新啟動的項目需要進行訂單狀態的管理,我著手將Spring StateMachine接入了進來,管理購物訂單狀態,不得不說,Spring StateMachine全家桶的文檔寫的是不錯,並且Spring StateMachine也是有官方背書的。但是,它實在是太”重“了,想要簡單修改一個訂單的狀態,需要十分複雜的代碼來實現。具體就不在這裡展開了,不然我感覺可以吐槽一整天。

說到底Spring StateMachine上手難度非常大,如果沒有用來做重型狀態機的需求,十分不推薦普通的小項目進行接入。

最最重要的是,由於Spring StateMachine狀態機實例不是無狀態的,無法做到線程安全,所以代碼要麼需要使用鎖同步,要麼需要用Threadlocal,非常的痛苦和難用。 例如下麵的Spring StateMachine代碼就用了重量級鎖保證線程安全,在高併發的互聯網應用中,這種代碼留的隱患非常大。

private synchronized boolean sendEvent(Message<PurchaseOrderEvent> message, OrderEntity orderEntity) {
        boolean result = false;
        try {
            stateMachine.start();
            // 嘗試恢復狀態機狀態
            persister.restore(stateMachine, orderEntity);
            // 執行事件
            result = stateMachine.sendEvent(message);
            // 持久化狀態機狀態
            persister.persist(stateMachine, (OrderEntity) message.getHeaders().get("purchaseOrder"));
        } catch (Exception e) {
            log.error("sendEvent error", e);
        } finally {
            stateMachine.stop();
        }
        return result;
    }

吃了一次虧後,我再一次在網上翻閱各種Java狀態機的實現,有大的開源項目,也有小而美的個人實現。結果在COLA架構中發現了COLA還寫了一套狀態機實現。COLA的作者給我們提供了一個無狀態的,輕量化的狀態機,接入十分簡單。並且由於無狀態的特點,可以做到線程安全,支持電商的高併發場景。

COLA是什麼?如果你還沒聽說過COLA,不妨看一看我之前的文章,傳送門如下:

https://mp.weixin.qq.com/s/07i3FjcFrZ8rxBCACgeWVQ

如果你需要在項目中引入狀態機,此時此刻,我會推薦使用COLA狀態機。

COLA狀態機介紹

COLA狀態機是在Github開源的,作者也寫了介紹文章:

https://blog.csdn.net/significantfrank/article/details/104996419

官方文章的前半部分重點介紹了DSL(Domain Specific Languages),這一部分比較抽象和概念化,大家感興趣,可以前往原文查看。我精簡一下DSL的主要含義:

什麼是DSL? DSL是一種工具,它的核心價值在於,它提供了一種手段,可以更加清晰地就系統某部分的意圖進行溝通。

比如正則表達式,/\d{3}-\d{3}-\d{4}/就是一個典型的DSL,解決的是字元串匹配這個特定領域的問題。

文章的後半部分重點闡述了作者為什麼要做COLA狀態機?想必這也是讀者比較好奇的問題。我幫大家精簡一下原文的表述:

  • 首先,狀態機的實現應該可以非常的輕量,最簡單的狀態機用一個Enum就能實現,基本是零成本。
  • 其次,使用狀態機的DSL來表達狀態的流轉,語義會更加清晰,會增強代碼的可讀性和可維護性
  • 開源狀態機太複雜: 就我們的項目而言(其實大部分項目都是如此)。我實在不需要那麼多狀態機的高級玩法:比如狀態的嵌套(substate),狀態的並行(parallel,fork,join)、子狀態機等等
  • 開源狀態機性能差: 這些開源的狀態機都是有狀態的(Stateful)的,因為有狀態,狀態機的實例就不是線程安全的,而我們的應用伺服器是分散式多線程的,所以在每一次狀態機在接受請求的時候,都不得不重新build一個新的狀態機實例。

所以COLA狀態機設計的目標很明確,有兩個核心理念:

  1. 簡潔的僅支持狀態流轉的狀態機,不需要支持嵌套、並行等高級玩法。
  2. 狀態機本身需要是Stateless(無狀態)的,這樣一個Singleton Instance就能服務所有的狀態流轉請求了。

COLA狀態機的核心概念如下圖所示,主要包括:

State:狀態
Event:事件,狀態由事件觸發,引起變化
Transition:流轉,表示從一個狀態到另一個狀態
External Transition:外部流轉,兩個不同狀態之間的流轉
Internal Transition:內部流轉,同一個狀態之間的流轉
Condition:條件,表示是否允許到達某個狀態
Action:動作,到達某個狀態之後,可以做什麼
StateMachine:狀態機

COLA狀態機原理

這一小節,我們先講幾個COLA狀態機最重要兩個部分,一個是它使用的連貫介面,一個是狀態機的註冊和使用原理。如果你暫時對它的實現原理不感興趣,可以直接跳過本小節,直接看後面的實戰代碼部分。

PS:講解的代碼版本為cola-component-statemachine 4.2.0-SNAPSHOT

下圖展示了COLA狀態機的源代碼目錄,可以看到非常的簡潔。

1. 連貫介面 Fluent Interfaces

COLA狀態機的定義使用了連貫介面Fluent Interfaces,連貫介面的一個重要作用是,限定方法調用的順序。比如,在構建狀態機的時候,我們只有在調用了from方法後,才能調用to方法,Builder模式沒有這個功能。

下圖中可以看到,我們在使用的時候是被嚴格限制的:

StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE2)
                .on(Events.EVENT1)
                .when(checkCondition())
                .perform(doAction());

這是如何實現的?其實是使用了Java介面來實現。

2. 狀態機註冊和觸發原理

這裡簡單梳理一下狀態機的註冊和觸發原理。

用戶執行如下代碼來創建一個狀態機,指定一個MACHINE_ID:

StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);

COLA會將該狀態機在StateMachineFactory類中,放入一個ConcurrentHashMap,以狀態機名為key註冊。

static Map<String /* machineId */, StateMachine> stateMachineMap = new ConcurrentHashMap<>();

註冊好後,用戶便可以使用狀態機,通過類似下方的代碼觸髮狀態機的狀態流轉:

stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("1"));

內部實現如下:

  1. 首先判斷COLA狀態機整個組件是否初始化完成。
  2. 通過routeTransition尋找是否有符合條件的狀態流轉。
  3. transition.transit執行狀態流轉。

transition.transit方法中:

檢查本次流轉是否符合condition,符合,則執行對應的action。

COLA狀態機實戰

**PS:以下實戰代碼取自COLA官方倉庫測試類

一、狀態流轉使用示例

  1. 從單一狀態流轉到另一個狀態
@Test
public void testExternalNormal(){
    StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
    builder.externalTransition()
            .from(States.STATE1)
            .to(States.STATE2)
            .on(Events.EVENT1)
            .when(checkCondition())
            .perform(doAction());

    StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);
    States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
    Assert.assertEquals(States.STATE2, target);
}

private Condition<Context> checkCondition() {
		return (ctx) -> {return true;};
}

private Action<States, Events, Context> doAction() {
    return (from, to, event, ctx)->{
        System.out.println(ctx.operator+" is operating "+ctx.entityId+" from:"+from+" to:"+to+" on:"+event);
        };
}

可以看到,每次進行狀態流轉時,檢查checkCondition(),當返回true,執行狀態流轉的操作doAction()。

後面所有的checkCondition()和doAction()方法在下方就不再重覆貼出了。

  1. 從多個狀態流傳到新的狀態
@Test
public void testExternalTransitionsNormal(){
    StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
    builder.externalTransitions()
            .fromAmong(States.STATE1, States.STATE2, States.STATE3)
            .to(States.STATE4)
            .on(Events.EVENT1)
            .when(checkCondition())
            .perform(doAction());

    StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID+"1");
    States target = stateMachine.fireEvent(States.STATE2, Events.EVENT1, new Context());
    Assert.assertEquals(States.STATE4, target);
}
  1. 狀態內部觸發流轉
@Test
public void testInternalNormal(){
    StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
    builder.internalTransition()
            .within(States.STATE1)
            .on(Events.INTERNAL_EVENT)
            .when(checkCondition())
            .perform(doAction());
    StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID+"2");

    stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
    States target = stateMachine.fireEvent(States.STATE1, Events.INTERNAL_EVENT, new Context());
    Assert.assertEquals(States.STATE1, target);
}
  1. 多線程測試併發測試
@Test
public void testMultiThread(){
	buildStateMachine("testMultiThread");

  for(int i=0 ; i<10 ; i++){
  	Thread thread = new Thread(()->{
      StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
      States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
      Assert.assertEquals(States.STATE2, target);
      });
      thread.start();
    }


    for(int i=0 ; i<10 ; i++) {
      Thread thread = new Thread(() -> {
      StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
      States target = stateMachine.fireEvent(States.STATE1, Events.EVENT4, new Context());
      Assert.assertEquals(States.STATE4, target);
      });
      thread.start();
    }

    for(int i=0 ; i<10 ; i++) {
      Thread thread = new Thread(() -> {
      StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
      States target = stateMachine.fireEvent(States.STATE1, Events.EVENT3, new Context());
      Assert.assertEquals(States.STATE3, target);
      });
      thread.start();
  }

}

由於COLA狀態機時無狀態的狀態機,所以性能是很高的。相比起來,SpringStateMachine由於是有狀態的,就需要使用者自行保證線程安全了。

二、多分支狀態流轉示例

/**
* 測試選擇分支,針對同一個事件:EVENT1
* if condition == "1", STATE1 --> STATE1
* if condition == "2" , STATE1 --> STATE2
* if condition == "3" , STATE1 --> STATE3
*/
@Test
public void testChoice(){
  StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, Context> builder = StateMachineBuilderFactory.create();
  builder.internalTransition()
  .within(StateMachineTest.States.STATE1)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition1())
  .perform(doAction());
  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE2)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition2())
  .perform(doAction());
  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE3)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition3())
  .perform(doAction());

  StateMachine<StateMachineTest.States, StateMachineTest.Events, Context> stateMachine = builder.build("ChoiceConditionMachine");
  StateMachineTest.States target1 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("1"));
  Assert.assertEquals(StateMachineTest.States.STATE1,target1);
  StateMachineTest.States target2 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("2"));
  Assert.assertEquals(StateMachineTest.States.STATE2,target2);
  StateMachineTest.States target3 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("3"));
  Assert.assertEquals(StateMachineTest.States.STATE3,target3);
  }

可以看到,編寫一個多分支的狀態機也是非常簡單明瞭的。

三、通過狀態機反向生成PlantUml圖

沒想到吧,還能通過代碼定義好的狀態機反向生成plantUML圖,實現狀態機的可視化。(可以用圖說話,和產品對比下狀態實現的是否正確了。)

四、特殊使用示例

  1. 不滿足狀態流轉條件時的處理
@Test
public void testConditionNotMeet(){
  StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE2)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkConditionFalse())
  .perform(doAction());

  StateMachine<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> stateMachine = builder.build("NotMeetConditionMachine");
  StateMachineTest.States target = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new StateMachineTest.Context());
  Assert.assertEquals(StateMachineTest.States.STATE1,target);
}

可以看到,當checkConditionFalse()執行時,永遠不會滿足狀態流轉的條件,則狀態不會變化,會直接返回原來的STATE1。相關源碼在這裡:

  1. 重覆定義相同的狀態流轉
@Test(expected = StateMachineException.class)
public void testDuplicatedTransition(){
  StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE2)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition())
  .perform(doAction());

  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE2)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition())
  .perform(doAction());
}

會在第二次builder執行到on(StateMachineTest.Events.EVENT1)函數時,拋出StateMachineException異常。拋出異常在on()的verify檢查這裡,如下:

  1. 重覆定義狀態機
@Test(expected = StateMachineException.class)
public void testDuplicateMachine(){
  StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
  builder.externalTransition()
  .from(StateMachineTest.States.STATE1)
  .to(StateMachineTest.States.STATE2)
  .on(StateMachineTest.Events.EVENT1)
  .when(checkCondition())
  .perform(doAction());

  builder.build("DuplicatedMachine");
  builder.build("DuplicatedMachine");
}

會在第二次build同名狀態機時拋出StateMachineException異常。拋出異常的源碼在狀態機的註冊函數中,如下:

結語

為了不把篇幅拉得過長,在這裡無法詳細地橫向對比幾大主流狀態機(Spring Statemachine,Squirrel statemachine等)和COLA的區別,不過基於筆者在Spring Statemachine踩過的深坑,目前來看,COLA狀態機的簡潔設計適合用在訂單管理等小型狀態機的維護,如果你想要在你的項目中接入狀態機,又不需要嵌套、並行等高級玩法,那麼COLA是個十分合適的選擇。

我是後端工程師,蠻三刀醬。

持續的更新原創優質文章,離不開你的點贊,轉發和分享!

我的唯一技術公眾號:後端技術漫談


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 好消息,騰訊雲資料庫團隊智能調參CDBTune產品現已進入內測階段,歡迎資料庫愛好者、使用者、開發者前來測試。 CDBTune(cloud database tune)是基於2019至2021年間騰訊雲資料庫團隊連續發表兩篇頂級論文的研究成果,對雲資料庫進行調優的一整套解決方案,旨在充分藉助深度學習 ...
  • CITY表: Field Type ID number NAME VARCHAR2(17) COUNTRYCODE VARCHAR2(3) DISTRICT VARCHAR2(20) POPULATION number 1.Query all columns for all American cit ...
  • Mysql 5.7的安裝搭建 首先去到官方網站的下載鏈接中找到對應你Linux伺服器版本的mysql軟體包 https://dev.mysql.com/downloads/repo/yum/ 我使用的是CentOS7所以下載Red HAT Enterprise Linux 7版本的軟體包,然後跳轉至 ...
  • 華為開發者聯盟與艾瑞咨詢聯合發佈《2022年移動應用出海趨勢洞察白皮書》,本白皮書結合多種研究方法分析移動應用的出海吸引力、海外市場選擇、出海的痛點及挑戰,為現階段移動應用企業的出海戰略決策提供市場洞察,並針對出海挑戰呈現應對策略,助力移動應用開發者出海。 華為開發者聯盟一直致力於全方位聯接全球開發 ...
  • 華為帳號服務(Account Kit)為開發者提供簡單、安全的登錄授權功能,用戶不必輸入帳號、密碼和繁瑣驗證,就可以通過華為帳號快速登錄應用,即刻使用App。這篇文章收集了開發者們集成華為帳號服務中會遇到的典型問題,並給出瞭解決方法,希望為其他遇到類似問題的開發者提供參考。 1 .redirect_ ...
  • 一. 在iview中寫一個submenu <Col span="3" type="flex" v-if="showCids"> <Menu ref="menus" theme="light" active-name="0" @on-select="selectMenu" width="auto" > ...
  • 1.背景 業務需求,需要聯動多個平臺,涉及到各平臺的模擬登錄。 已知加密前明文且正常登錄。(無驗證碼要求) 某平臺驗證驗證方式為.\login介面POST一串json字元串 { "account": "********", "password": "uR+dmpMdF9MRXfkBG3wQ+w==" ...
  • 問題描述 前端頁面載入css,和js文件的時候,經常出現ERR_CONTENT_LENGTH_MISMATCH的報錯情況 定位問題 在單獨打開hearder中css,js的網路地址是能打開的,所以排除了最簡單的地址錯誤。前端項目是由nginx代理的,所以可以查看nginx的日誌,看看有無線索。 進入 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...