在平常的後端專案開發中,狀態機模式的使用其實沒有大家想象中那麼常見,筆者之前由於不在電商領域工作,很少在業務程式碼中用狀態機來管理各種狀態,一般都是手動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狀態機是在Github開源的,作者也寫了介紹文章:
https://blog.csdn.net/significantfrank/article/details/104996419
官方文章的前半部分重點介紹了DSL(Domain Specific Languages),這一部分比較抽象和概念化,大家感興趣,可以前往原文檢視。我精簡一下DSL的主要含義:
什麼是DSL? DSL是一種工具,它的核心價值在於,它提供了一種手段,可以更加清晰地就係統某部分的意圖進行溝通。
比如正規表示式,
/\d{3}-\d{3}-\d{4}/
就是一個典型的DSL,解決的是字串匹配這個特定領域的問題。
文章的後半部分重點闡述了作者為什麼要做COLA狀態機?想必這也是讀者比較好奇的問題。我幫大家精簡一下原文的表述:
所以COLA狀態機設計的目標很明確,有兩個核心理念:
COLA狀態機的核心概念如下圖所示,主要包括:
State:狀態
Event:事件,狀態由事件觸發,引起變化
Transition:流轉,表示從一個狀態到另一個狀態
External Transition:外部流轉,兩個不同狀態之間的流轉
Internal Transition:內部流轉,同一個狀態之間的流轉
Condition:條件,表示是否允許到達某個狀態
Action:動作,到達某個狀態之後,可以做什麼
StateMachine:狀態機
這一小節,我們先講幾個COLA狀態機最重要兩個部分,一個是它使用的連貫介面,一個是狀態機的註冊和使用原理。如果你暫時對它的實現原理不感興趣,可以直接跳過本小節,直接看後面的實戰程式碼部分。
PS:講解的程式碼版本為cola-component-statemachine 4.2.0-SNAPSHOT
下圖展示了COLA狀態機的原始碼目錄,可以看到非常的簡潔。
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介面來實現。
這裡簡單梳理一下狀態機的註冊和觸發原理。
使用者執行如下程式碼來建立一個狀態機,指定一個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"));
內部實現如下:
transition.transit方法中:
檢查本次流轉是否符合condition,符合,則執行對應的action。
**PS:以下實戰程式碼取自COLA官方倉庫測試類
@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()方法在下方就不再重複貼出了。
@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);
}
@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);
}
@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圖,實現狀態機的視覺化。(可以用圖說話,和產品對比下狀態實現的是否正確了。)
@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。相關原始碼在這裡:
@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檢查這裡,如下:
@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是個十分合適的選擇。
我是後端工程師,蠻三刀醬。
持續的更新原創優質文章,離不開你的點贊,轉發和分享!
我的唯一技術公眾號:後端技術漫談