得物商品狀態體系介紹
一 得物的商品體系
目前得物的商品分爲三種類型,分別是:新品、商品、草稿。但是隻有商品是可售賣的,新品和草稿都不是可售賣的。
新品有很多種創建的渠道,商品可以由新品選品通過後由系統自動生成,也可以由運營直接創建。而商品草稿是在商品被編輯後創建而來,草稿在更新審覈通過後,會重新覆蓋已有的商品信息。
新品
新品是企業賣家或個人賣家或者 ISV 等渠道申請的一種不可售賣的商品,需要運營選品審覈、商研審覈通過後,纔會變成可售賣的商品,精簡後的新品申請的流程大致如下圖所示:
商品
新品在審覈通過後,就變成了商品,商品的狀態有上架、下架、待審覈等多種,只有上架狀態的商品是可售賣的。
未上架的商品有兩種情況,一種是待補全信息,一種是待審覈,正常審覈通過的商品如果沒有特殊條件,會自動上架。
第 1 種待補全的商品,這種商品是新品在運營選品通過後,就進入了商品池,需要補全其他信息,然後再由商研後置審覈通過後進行上架。
第 2 種待審覈的商品,這種商品是在原來上架狀態的商品被下架並且編輯之後,等待審覈。
待審覈的商品還未上架過,所以沒有草稿,如果編輯該商品會直接修改商品庫裏的信息。
如果已經上架了,再來編輯商品,則會先生成草稿,草稿保存在草稿庫中,此時草稿的修改不會影響原商品。
新品上新的流程在迭代過程中也發生了變化:
1、現有 SPU 管理可以跳過商研審覈,直接創建商品上架,流程上是將商研審覈後置了;
2、得物運營自挖品,需要運營先在新品中提報,經過選品和商研審覈後,再去 SPU 管理補全商品資料再審覈上架。新品提報和補全資料這兩個流程希望可以進行合併,節約上架時效和上架成本。
草稿
草稿是在商品的基礎上編輯之後,生成的一個副本,草稿可以反覆編輯。
在管理後臺的頁面上體現爲,如果有 “草稿” 兩個字,則說明這條記錄是一個草稿,如果有 “編輯” 兩個字,則說明這條記錄是一個商品。
二 得物商品狀態流轉
目前得物的商品狀態共有:下架、上架、待補全、待審覈、審覈通過等等數十種狀態。
當對商品進行編輯時,會創建一條草稿記錄,記錄商品修改後的副本信息,保存在草稿庫中,其中草稿的狀態和商品原本的狀態是隔離的,草稿的狀態變更不會影響商品的狀態。
精簡後的各狀態之間的流轉如下圖所示:
從上圖可以看出,商品的狀態已經相當豐富,狀態之間的流轉也是錯綜複雜,並且還涉及到不同的商品類型之間的流轉。從系統後續的穩定性和可維護性來看,確實到了需要引入狀態機來維護商品狀態流轉的時機了。
三 狀態機圖
狀態機圖是一種行爲圖,它通過有限的狀態轉換來表示系統中的某些行爲。除此之外狀態機圖也可以用來表示系統的某種協議狀態。UML 2.4 中定義的兩種狀態機是:行爲狀態機和協議狀態機。具體可以參考 UML State Machine 中的定義。
核心組件
大體上狀態機有以下幾個核心的組件:
-
狀態
-
事件
-
流轉
-
條件
-
動作
通過這些組件共同來形成一個完整的狀態機:
如下圖所示,表示有兩個狀態,StateA 和 StateB,兩個狀態之間通過 EventA 和 EventB 事件進行流轉。在狀態扭轉之前需要滿足一定的條件,條件滿足後即可執行狀態流轉,並可執行狀態流轉後的動作。
狀態類型
在 UML 的定義中,狀態有三種類型:簡單狀態、組合狀態、子狀態機狀態。
其中簡單狀態、組合狀態比較好理解,如下圖所示,組合狀態將多個簡單狀態進行組合封裝,形成一個新的複雜的狀態,內部狀態和它的內部內容被定義它們的狀態機所包含,如下圖所示:
但是子狀態機狀態 (以下我們用子機狀態來表示) 相對比較複雜,子機狀態在語義上等同於組合狀態。子機狀態機的區域是組合狀態的區域。
進入、退出和其他行爲動作以及內部轉換被定義爲狀態的一部分。子機狀態是一種分解機制,它允許對公共行爲進行分解並複用。子狀態機是一個狀態機定義可以被多次複用的方式。它也需要將進入和離開遷移綁定到內部頂點上,這一點與封裝組合狀態類似。
子機狀態最重要的作用就是封裝和複用,概念理解起來比較晦澀難懂,下面我們用一張圖來描述:
雖然 UML 在狀態機的定義中定義了這麼多種狀態,但實際上我們只需要簡單狀態就夠用了。
四 狀態機選型介紹
開源的狀態機引擎有很多,目前在 Github 上的 Top 2 狀態機實現中,一個是 Spring StateMachine,一個是 Squirrel StateMachine。他們的優點是功能很完備,缺點也是功能很完備。
就我們的項目而言,不需要那麼多狀態機的高級玩法,其實大部分項目都是如此:比如狀態的嵌套(nested state),狀態的並行(parallel,fork,join)、子狀態機等等。
網上已經有非常多的狀態機選型的文章了,這裏不再長篇贅述,只做簡單的介紹。
Enum StateMachine
在看開源的狀態機引擎之前,我們先看一下,通過枚舉實現一個狀態機的最簡單方式。
枚舉類型因爲自身的線程安全性保障和高可讀性特性,是簡單狀態機的首選。
首先我們定義一個枚舉,表示商品的狀態,並在枚舉中定義一個狀態流轉的方法,其中狀態流轉的抽象方法中接收 3 個參數:
-
期望流轉到的目標狀態
-
狀態流轉的條件
-
狀態流轉後的動作
public interface StateCondition {
// 檢查是否能流轉到目標狀態
boolean check(CommodityState target);
}
public interface StateAction {
void doAction();
}
狀態機的枚舉定義如下:
public enum CommodityState {
// 待審覈
TO_AUDIT {
@Override
StateCondition getCondition() {return new ToAuditStateCondition();}
@Override
StateAction getAction() {return new ToAuditStateAction();}
},
// 已上架
ON_SHELF {
@Override
StateCondition getCondition() {return new OnShelfStateCondition();}
@Override
StateAction getAction() {return new OnShelfStateAction();}
},
// 已下架
OFF_SHELF {
@Override
StateCondition getCondition() {return new OffShelfStateCondition();}
@Override
StateAction getAction() {return new OffShelfStateAction();}
};
boolean transition(CommodityState target) {
StateCondition condition = getCondition();
if (condition.check(target)) {
StateAction action = getAction();
action.doAction();
return true;
}
throw new IllegalArgumentException("當前狀態不符合流轉條件");
}
abstract StateCondition getCondition();
abstract StateAction getAction();
}
具體的條件檢查和執行的動作,都定義到每個狀態具體的實現類中。
Spring StateMachine
Spring StateMachine 是 Spring 官方提供的狀態機實現。
先從狀態機的定義入手,StateMachine<States, Events>,其中:
-
StateMachine:狀態機模型
-
State:S - 狀態,一般定義爲一個枚舉類,如創建、待風控審覈、待支付等狀態
-
Event:E - 事件,同樣定義成一個枚舉類,如訂單創建、訂單審覈、支付等,代表一個動作。一個狀態機的定義就由這兩個主要的元素組成,狀態及對對應的事件(動作)。
Spring StateMachine 中的相關概念:
-
Transition: 節點,是組成狀態機引擎的核心
-
Source:節點的當前狀態
-
Target:節點的目標狀態
-
Event:觸發節點從當前狀態到目標狀態的動作
-
Guard:起校驗功能,一般用於校驗是否可以執行後續 Action
-
Action:用於實現當前節點對應的業務邏輯處理
以下是一些核心組件:
Spring StateMachine 的核心實現:
對於節點配置,可以看個簡單的例子:
builder.configureTransitions() // 配置節點
// 表示source target兩種狀態不同
.withExternal()
// 當前節點狀態
.source(SOURCE)
// 目標節點狀態
.target(TARGET)
// 導致當前變化的動作/事件
.event(BizOrderStatusChangeEventEnum.EVT_CREATE)
// 執行當前狀態變更導致的業務邏輯處理,以及出異常時的處理
.action(orderCreateAction, errorHandlerAction);
其中有幾種可選的類型:
-
WithExternal 是當 Source 和 Target 不同時的寫法,如上例子。
-
WithInternal 當 Source 和 Target 相同時的串聯寫法,比如付款失敗時,付款前及付款後都是待付款狀態。
-
WithChoice 當執行一個動作,可能導致多種結果時,可以選擇使用 Choice+Guard 來跳轉。
更詳細的進行 Spring 狀態機的配置,可以參考這篇文章:https://www.jianshu.com/p/b0c9e4f9d769
Squirrel StateMachine
Squirrel-Foundation 是一款很優秀的開源產品,推薦大家閱讀以下它的源碼。相較於 Spring statemachine,Squirrel 的實現更爲輕量,設計域也很清晰,對應的文檔以及測試用例也很豐富。
核心組件:
Squirrel StateMachine 的核心實現:
Squirrel 的事件處理模型與 Spring-Statemachine 比較類似,Squirrel 的事件執行器的作用點粒度更細,通過預處理,將一個狀態遷移分解成 Exit Trasition Entry 這三個 Action Event,再遞交給執行器分別執行 (這個設計挺不錯)。
怎樣配置並使用 Squirrel StateMachine,可以參考這篇文章:https://blog.csdn.net/footless_bird/article/details/115797710
Cola StateMachine
開源狀態機都是有狀態的(Stateful)的,有狀態意味着多線程併發情況下如果是單個實例就容易出現線程安全問題。
如今我們的系統普遍都是分佈式部署,不得不考慮多線程的問題,因爲每來一個請求就需要創建一個狀態機實例 (per statemachine per request)。如果某些狀態機它的構建過程很複雜,並且當下 QPS 又很高的話,往往會造成系統的性能瓶頸。
爲此阿里出了一個開源的狀態機:Cola-StateMachine
當時他們團隊也想搞個狀態機來減負,經過深思熟慮、不斷類比之後他們考慮自研。希望能設計出一款功能相對簡單、性能良好的開源狀態機;最後命名爲 Cola-ComPonent-Statemachine。
Cola-StateMachine 最重要的特點是,狀態機的設計是無狀態的,並且內部實現了 DSL 語法,通過流式 API 限定了方法調用的順序。
分析一下市面上的開源狀態機引擎,不難發現,它們之所以有狀態,主要是在狀態機裏面維護了兩個狀態:初始狀態(Initial State)和當前狀態(Current State),如果我們能把這兩個實例變量去掉的話,就可以實現無狀態,從而實現一個狀態機只需要有一個 Instance 就夠了。
關鍵是這兩個狀態可以不要嗎?當然可以,唯一的副作用是,我們沒辦法獲取到狀態機 Instance 的 Current State。然而,我也不需要知道,因爲我們使用狀態機,僅僅是接受一下 Source State,Check 一下 Condition,Execute 一下 Action,然後返回 Target State 而已。它只是實現了一個狀態流轉的 DSL 表達,僅此而已,全程操作完全可以是無狀態的。
具體舉例如下:
// 構建一個狀態機(生產場景下,生產場景可以直接初始化一個Bean)
StateMachineBuilder<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context>
builder = StateMachineBuilderFactory.create();
// 外部流轉(兩個不同狀態的流轉)
builder.externalTransition()
.from(SOURCE)//原來狀態
.to(TARGET)//目標狀態
.on(EVENT1)//基於此事件觸發
.when(checkCondition1())//前置過濾條件
.perform(doAction());//滿足條件,最終觸發的動作
更詳細的介紹 Cola StateMachine 的資料,可以參考作者的介紹:https://blog.csdn.net/significantfrank/article/details/104996419
五 狀態機性能評測
本次對比的是 Spring StateMachine 和 Cola StateMachine 的性能,爲了儘量避免其他邏輯的影響,我在 Action 和 Condition 的實現類中,均是空實現,保證只評測兩個框架本身的性能。
本次評測的兩個框架的版本如下:
<dependency>
<groupId>com.alibaba.cola</groupId>
<artifactId>cola-component-statemachine</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.1</version>
</dependency>
準備測試代碼
Spring StateMachine
將主要的代碼都封裝到兩個類中:SpuStateMachineConfig 和 SpuStateMachineService
首先是 SpuStateMachineConfig,主要是對 Spring StateMachine 進行配置。
/**
* 狀態機 核心配置
*/
@Configuration
public class SpuStateMachineConfig extends EnumStateMachineConfigurerAdapter<SpuStatesEnum, SpuEventsEnum> {
public final static String DEFAULT_MACHINEID = "spring/machine/default/machineid";
private final SpuStateMachinePersist spuStateMachinePersist = new SpuStateMachinePersist();
private final StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> stateMachinePersister = new DefaultStateMachinePersister<>(spuStateMachinePersist);
private final DefaultSpuGuard defaultSpuGuard = new DefaultSpuGuard();
private final DefaultSpuErrorAction defaultSpuErrorAction = new DefaultSpuErrorAction();
private final DefaultSpuSuccessAction defaultSpuSuccessAction = new DefaultSpuSuccessAction();
private final SpuCreateDraftSuccessAction spuCreateDraftSuccessAction = new SpuCreateDraftSuccessAction();
private final SpuCancelDraftSuccessAction spuCancelDraftSuccessAction = new SpuCancelDraftSuccessAction();
public StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> getSpuMachinePersister() {
return stateMachinePersister;
}
@Bean
public StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> spuMachinePersister() {
return getSpuMachinePersister();
}
@Override
public void configure(StateMachineConfigurationConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
configMachineId(config, DEFAULT_MACHINEID);
}
@Override
public void configure(StateMachineStateConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
configureStates(config);
}
@Override
public void configure(StateMachineTransitionConfigurer<SpuStatesEnum, SpuEventsEnum> transitions) throws Exception {
configureTransitions(transitions);
}
@Bean(name = "spuStateMachineFactory")
public StateMachineFactory<SpuStatesEnum, SpuEventsEnum> spuStateMachineFactory() throws Exception {
StateMachineConfigBuilder<SpuStatesEnum, SpuEventsEnum> configBuilder = new StateMachineConfigBuilder<SpuStatesEnum, SpuEventsEnum>();
// 通過apply方法將Configurer設置進去,this正好實現了
// 也可以自定義實現configurer,比如:new BuilderStateMachineConfigurerAdapter<>();
configBuilder.apply(this);
StateMachineConfig<SpuStatesEnum, SpuEventsEnum> stateMachineConfig = configBuilder.getOrBuild();
StateMachineModel<SpuStatesEnum, SpuEventsEnum> machineModel = getMachineModel(stateMachineConfig);
StateMachineModelFactory<SpuStatesEnum, SpuEventsEnum> factory = stateMachineConfig.getModel().getFactory();
return new ObjectStateMachineFactory<>(machineModel, factory);
}
private static StateMachineModel<SpuStatesEnum, SpuEventsEnum> getMachineModel(StateMachineConfig<SpuStatesEnum, SpuEventsEnum> stateMachineConfig) {
StatesData<SpuStatesEnum, SpuEventsEnum> stateMachineStates = stateMachineConfig.getStates();
TransitionsData<SpuStatesEnum, SpuEventsEnum> stateMachineTransitions = stateMachineConfig.getTransitions();
ConfigurationData<SpuStatesEnum, SpuEventsEnum> stateMachineConfigurationConfig = stateMachineConfig.getStateMachineConfigurationConfig();
// 設置StateMachineModel
return new DefaultStateMachineModel<>(stateMachineConfigurationConfig, stateMachineStates, stateMachineTransitions);
}
}
主要執行的核心配置如下,包括配置狀態機,添加所有支持的狀態,添加狀態的變遷。
private void configure(StateMachineBuilder.Builder<SpuStatesEnum, SpuEventsEnum> builder, String machineId) {
try {
// 設置狀態機id
configMachineId(builder.configureConfiguration(), machineId);
// 添加狀態
configureStates(builder.configureStates());
// 添加狀態變遷
configureTransitions(builder.configureTransitions());
} catch (Exception e) {
e.printStackTrace();
}
}
private void configMachineId(StateMachineConfigurationConfigurer<SpuStatesEnum, SpuEventsEnum> config, String machineId) throws Exception {
config.withConfiguration().machineId(machineId);
}
private void configureStates(StateMachineStateConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
config.withStates()
.initial(SpuStatesEnum.NONE)
.states(EnumSet.allOf(SpuStatesEnum.class));
}
private void configureTransitions(StateMachineTransitionConfigurer<SpuStatesEnum, SpuEventsEnum> transitions) throws Exception {
//====創建草稿====
transitions
.withExternal()
// 初始狀態
.source(SpuStatesEnum.INIT)
//目標狀態
.target(SpuStatesEnum.DRAFT)
// 事件
.event(SpuEventsEnum.CREATE_DRAFT)
// 過濾條件
.guard(defaultSpuGuard)
// 動作
.action(spuCreateDraftSuccessAction, defaultSpuErrorAction)
//====創建SPU====
.and().withExternal()
.source(SpuStatesEnum.INIT)
.target(SpuStatesEnum.NEW)
.event(SpuEventsEnum.CREATE_SPU)
.guard(defaultSpuGuard)
.action(defaultSpuSuccessAction, defaultSpuErrorAction)
//====創建SPU (基於草稿創建spu )====
.and().withExternal()
.source(SpuStatesEnum.DRAFT)
.target(SpuStatesEnum.NEW)
.event(SpuEventsEnum.CREATE_SPU)
.guard(defaultSpuGuard)
.action(defaultSpuSuccessAction, defaultSpuErrorAction)
//====提交審覈====
.and().withExternal()
.source(SpuStatesEnum.NEW)
.target(SpuStatesEnum.PENDING_REVIEW)
.event(SpuEventsEnum.INITIATE_AUDIT)
.guard(defaultSpuGuard)
.action(defaultSpuSuccessAction, defaultSpuErrorAction)
//====審覈通過====
.and().withExternal()
.source(SpuStatesEnum.PENDING_REVIEW)
.target(SpuStatesEnum.CM_APPROVED_PASS)
.event(SpuEventsEnum.REVIEW_PASS)
.guard(defaultSpuGuard)
.action(defaultSpuSuccessAction, defaultSpuErrorAction)
//====審覈失敗====
.and().withExternal()
.source(SpuStatesEnum.PENDING_REVIEW)
.target(SpuStatesEnum.CM_APPROVED_REJECTION)
.event(SpuEventsEnum.REVIEW_REJECTION)
.guard(defaultSpuGuard)
.action(defaultSpuSuccessAction, defaultSpuErrorAction)
// 刪除草稿
.and().withExternal()
.source(SpuStatesEnum.DRAFT)
.target(SpuStatesEnum.CANCEL)
.event(SpuEventsEnum.CANCEL)
.guard(defaultSpuGuard)
.action(spuCancelDraftSuccessAction, defaultSpuErrorAction)
// 刪除SPU
.and().withExternal()
.source(SpuStatesEnum.NEW)
.target(SpuStatesEnum.CANCEL)
.event(SpuEventsEnum.CANCEL)
.guard(defaultSpuGuard)
.action(spuCancelDraftSuccessAction, defaultSpuErrorAction)
;
}
然後是在 SpuStateMachineService 中封裝狀態機的調用入口,並且在 SpuStateMachineService 中會啓動 Spring 容器。
/**
* 狀態機 核心處理 類
*/
public class SpuStateMachineService {
private final ApplicationContext applicationContext;
private final StateMachineFactory<SpuStatesEnum, SpuEventsEnum> spuStateMachineFactory;
private final StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> spuStateMachinePersister;
public SpuStateMachineService(String machineId) {
// 啓動Spring容器,獲取 ApplicationContext 對象
applicationContext = new AnnotationConfigApplicationContext(SpuStateMachineConfig.class);
spuStateMachineFactory = applicationContext.getBean(StateMachineFactory.class);
spuStateMachinePersister = applicationContext.getBean(StateMachinePersister.class);
}
/**
* 發送事件
*
* @param event
* @param context
* @return
*/
public boolean sendEvent(SpuEventsEnum event, SpuMessageContext context) {
// 利用隨記ID創建狀態機,創建時沒有與具體定義狀態機綁定
StateMachine<SpuStatesEnum, SpuEventsEnum> stateMachine = spuStateMachineFactory.getStateMachine(SpuStateMachineConfig.DEFAULT_MACHINEID);
try {
// restore
spuStateMachinePersister.restore(stateMachine, context);
// 構建 mesage
Message<SpuEventsEnum> message = MessageBuilder.withPayload(event)
.setHeader("request", context)
.build();
// 發送事件,返回是否執行成功
boolean success = stateMachine.sendEvent(message);
if (success) {
spuStateMachinePersister.persist(stateMachine, context);
}
return success;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("狀態機處理未執行成功", e);
} finally {
stateMachine.stop();
}
}
}
Cola StateMachine
將主要的代碼也都封裝到兩個類中:ColaStateMachineConfig 和 ColaStateMachineService。
首先是 ColaStateMachineConfig,主要負責 Cola StateMachine 的配置:
public class ColaStateMachineConfig<Context> {
public StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> createBuilder() {
StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> builder = StateMachineBuilderFactory.create();
// 創建草稿
builder.externalTransition()
.from(SpuStateEnum.INIT)
.to(SpuStateEnum.DRAFT)
.on(SpuEventEnum.CREATE_DRAFT)
.when(SpuEventEnum.CREATE_DRAFT.getCondition())
.perform(SpuEventEnum.CREATE_DRAFT.getAction());
// 創建SPU
builder.externalTransition()
.from(SpuStateEnum.INIT)
.to(SpuStateEnum.NEW)
.on(SpuEventEnum.CREATE_SPU)
.when(SpuEventEnum.CREATE_SPU.getCondition())
.perform(SpuEventEnum.CREATE_SPU.getAction());
// 創建SPU(基於草稿)
builder.externalTransition()
.from(SpuStateEnum.DRAFT)
.to(SpuStateEnum.NEW)
.on(SpuEventEnum.CREATE_SPU)
.when(SpuEventEnum.CREATE_SPU.getCondition())
.perform(SpuEventEnum.CREATE_SPU.getAction());
// 提交審覈
builder.externalTransition()
.from(SpuStateEnum.NEW)
.to(SpuStateEnum.PENDING_REVIEW)
.on(SpuEventEnum.INITIATE_AUDIT)
.when(SpuEventEnum.INITIATE_AUDIT.getCondition())
.perform(SpuEventEnum.INITIATE_AUDIT.getAction());
// 審覈通過
builder.externalTransition()
.from(SpuStateEnum.PENDING_REVIEW)
.to(SpuStateEnum.CM_APPROVED_PASS)
.on(SpuEventEnum.REVIEW_PASS)
.when(SpuEventEnum.REVIEW_PASS.getCondition())
.perform(SpuEventEnum.REVIEW_PASS.getAction());
// 審覈拒絕
builder.externalTransition()
.from(SpuStateEnum.PENDING_REVIEW)
.to(SpuStateEnum.CM_APPROVED_REJECTION)
.on(SpuEventEnum.REVIEW_REJECTION)
.when(SpuEventEnum.REVIEW_REJECTION.getCondition())
.perform(SpuEventEnum.REVIEW_REJECTION.getAction());
// 刪除SPU
builder.externalTransition()
.from(SpuStateEnum.DRAFT)
.to(SpuStateEnum.CANCEL)
.on(SpuEventEnum.CANCEL)
.when(SpuEventEnum.CANCEL.getCondition())
.perform(SpuEventEnum.CANCEL.getAction());
return builder;
}
}
然後是 ColaStateMachineService,主要是封裝了狀態機的調用入口:
public class ColaStateMachineService<Context>
{
private final ColaStateMachineConfig<Context> config;
private final StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> stateMachineBuilder;
private final StateMachine<SpuStateEnum, SpuEventEnum, Context> stateMachine;
public ColaStateMachineService(String machineId) {
config = new ColaStateMachineConfig<>();
stateMachineBuilder = config.createBuilder();
stateMachine = stateMachineBuilder.build(machineId);
}
public StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> getStateMachineBuilder() {
return stateMachineBuilder;
}
public StateMachine<SpuStateEnum, SpuEventEnum, Context> getStateMachine() {
return stateMachine;
}
public SpuStateEnum sendEvent(SpuStateEnum source, SpuEventEnum event, Context context) {
return getStateMachine().fireEvent(source, event, context);
}
}
準備基準測試代碼
基準測試是從吞吐量的維度做評測,預熱 2 輪,使用 2 個進程,每個進程中有 8 個線程進行測試。
Spring StateMachine
/**
* 基準測試
*
* @auther houyi.wh
* @date 2023-10-18 14:10:18
* @since 0.0.1
*/
@Warmup(iterations = 2)
@BenchmarkMode({Mode.Throughput})
@Measurement(iterations = 2, time = 1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2)
@Threads(8)
@State(Scope.Benchmark)
public class SpringStateMachineBench {
private SpuStateMachineService stateMachineService;
@Setup
public void prepare() {
stateMachineService = new SpuStateMachineService("commodity-machine");
}
@Benchmark
public void test_sendEvent() {
SpuMessageContext entity = new SpuMessageContext("122312", "spu-1222", "https://111.baae.com/1241241.mp4");
// 創建SPU,從INIT --> CREATE_SPU,如果符合條件則會執行doAction,並返回CREATE_SPU的狀態,否則返回INIT
boolean isSuccess = stateMachineService.sendEvent(SpuEventsEnum.CREATE_SPU, entity);
}
/**
* 執行基準測試
*
* @param args
* @throws RunnerException
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SpringStateMachineBench.class.getSimpleName())
.output("/Users/admin/Downloads/benchmark/spring-state-machine-benchmark.txt")
.build();
new Runner(opt).run();
}
}
Cola StateMachine
/**
* 基準測試
*
* @auther houyi.wh
* @date 2023-10-18 14:10:18
* @since 0.0.1
*/
@Warmup(iterations = 2)
@BenchmarkMode({Mode.Throughput})
@Measurement(iterations = 2, time = 1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2)
@Threads(8)
@State(Scope.Benchmark)
public class ColaStateMachineBench {
private ColaStateMachineService<SpuEntity> stateMachineService;
@Setup
public void prepare() {
stateMachineService = new ColaStateMachineService<>("commodity-machine");
}
@Benchmark
public void test_sendEvent() {
SpuEntity entity = new SpuEntity("122312", "spu-1222", "https://111.baae.com/1241241.mp4");
// 創建SPU,從INIT --> CREATE_SPU,如果符合條件則會執行doAction,並返回CREATE_SPU的狀態,否則返回INIT
SpuStateEnum spuStateEnum = stateMachineService.sendEvent(SpuStateEnum.INIT, SpuEventEnum.CREATE_SPU, entity);
}
/**
* 執行基準測試
*
* @param args
* @throws RunnerException
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ColaStateMachineBench.class.getSimpleName())
.output("/Users/admin/Downloads/benchmark/cola-state-machine-benchmark.txt")
.build();
new Runner(opt).run();
}
}
測試結果彙總
測試結果對比如下,單位是每毫秒執行的次數,可以看到 Cola 是 Spring 的 1449 倍。
PS:由於這裏測試的是框架本身的性能,doAction 中都是空實現,如果 doAction 使用實際的業務場景,根據木桶原理,最低的木板將決定木桶水位的高低,所以當 doAction 中有實際的 IO 操作時,兩個框架的性能將會被 IO 操作的 RT 所拉齊。
具體的測試結果如下:
Spring StateMachine
JMH version: 1.35
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=49634:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.shizhuang.duapp.statemachine.benchmark.SpringStateMachineBench.test_sendEvent
# Run progress: 0.00% complete, ETA 00:00:44
# Fork: 1 of 2
# Warmup Iteration 1: 17.855 ops/ms
# Warmup Iteration 2: 39.979 ops/ms
Iteration 1: 32.060 ops/ms
Iteration 2: 31.712 ops/ms
# Run progress: 50.00% complete, ETA 00:00:29
# Fork: 2 of 2
# Warmup Iteration 1: 16.947 ops/ms
# Warmup Iteration 2: 41.405 ops/ms
Iteration 1: 38.253 ops/ms
Iteration 2: 40.171 ops/ms
Result "com.shizhuang.duapp.statemachine.benchmark.SpringStateMachineBench.test_sendEvent":
35.549 ±(99.9%) 27.813 ops/ms [Average]
(min, avg, max) = (31.712, 35.549, 40.171), stdev = 4.304
CI (99.9%): [7.736, 63.362] (assumes normal distribution)
# Run complete. Total time: 00:00:58
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
SpringStateMachineBench.test_sendEvent thrpt 4 35.549 ± 27.813 ops/ms
Cola StateMachine
# JMH version: 1.35
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=64954:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.shizhuang.duapp.statemachine.benchmark.ColaStateMachineBench.test_sendEvent
# Run progress: 0.00% complete, ETA 00:00:44
# Fork: 1 of 2
# Warmup Iteration 1: 46019.377 ops/ms
# Warmup Iteration 2: 48460.086 ops/ms
Iteration 1: 53695.798 ops/ms
Iteration 2: 44157.738 ops/ms
# Run progress: 50.00% complete, ETA 00:00:28
# Fork: 2 of 2
# Warmup Iteration 1: 46100.542 ops/ms
# Warmup Iteration 2: 54736.678 ops/ms
Iteration 1: 49838.690 ops/ms
Iteration 2: 55205.836 ops/ms
Result "com.shizhuang.duapp.statemachine.benchmark.ColaStateMachineBench.test_sendEvent":
50724.516 ±(99.9%) 31836.478 ops/ms [Average]
(min, avg, max) = (44157.738, 50724.516, 55205.836), stdev = 4926.730
CI (99.9%): [18888.038, 82560.993] (assumes normal distribution)
# Run complete. Total time: 00:00:56
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
ColaStateMachineBench.test_sendEvent thrpt 4 50724.516 ± 31836.478 ops/ms
六 基本功能評測
持久化一方面是好事,另一方面是壞事。
好事是因爲持久化策略可以應對分佈式系統的故障,每個實體對象在做狀態變遷之前,可以從持久化的存儲中獲取該實體之前的狀態,不用擔心狀態的丟失。
壞事是因爲爲了要保證狀態機的狀態,每次狀態變遷之前都需要先恢復當前的狀態,這個操作是非常消耗性能的。
Cola StateMachine 將 StateMachine 的實例定義爲無狀態 (Stateless) 的,狀態的變遷不依賴當前 StateMachine 實例的狀態,所以也就不需要持久化的問題。系統發生故障時,StateMachine 的實例也不需要重建,只需要對狀態變遷做重試即可,狀態是否能夠變遷是在 Condition 中定義的,跟 StateMachine 的實例沒有直接的關係。
七 接入成本評測
八 擴展能力評測
九 狀態機對比總結
下面是一份詳細的 Spring StateMachine、Squirrel StateMachine、Cola StateMachine 對比:
綜合來看,三個狀態機框架都有自己的優勢和適用場景。Spring StateMachine 更適合應用於複雜業務場景,適合 Spring 生態中的應用;Squirrel StateMachine 更適合快速建模、輕量級場景;Cola StateMachine 更偏向於分佈式系統和領域驅動設計。根據實際需求和項目特點選擇適合的狀態機框架更爲重要。
十 商品域對於狀態機的訴求
商品域對於狀態機的訴求主要是希望:
-
能夠更清晰、更合理的管理和維護商品的狀態。
-
保證狀態的變遷是符合業務場景的,不能產生錯誤的狀態變遷。
-
解決商品狀態流轉的過程管控的問題,將複雜的狀態流轉從耦合的業務中提取出來,讓狀態變遷的邏輯單獨實現,便於後續狀態的維護和擴展。
商品域對於狀態機有以下這些使用場景:
-
商品狀態的狀態值較多,狀態之間的變遷較混亂,需要將散落在各個代碼裏維護不清晰的狀態變遷統一維護。
-
SPU 的有多個表示狀態的字段,需要在一個場景中同時維護多個狀態字段的狀態變遷。
十一 狀態機選型總結
根據各狀態機的功能、性能、接入成本、擴展能力,並結合商品領域對於狀態機的使用訴求,主要是希望:
-
能夠更清晰、更合理的管理和維護商品的狀態。
-
保證狀態的變遷是符合業務場景的,不能產生錯誤的狀態變遷。
-
解決商品狀態流轉的過程管控的問題,將複雜的狀態流轉從耦合的業務中提取出來,讓狀態變遷的邏輯單獨實現,便於後續狀態的維護和擴展。
另外狀態機的性能不是特別關注的點,對於複雜的業務場景的支持是特別關注的點,綜合來看,商品領域最終決定選擇 Spring StateMachine 作爲狀態機的框架。
參考資料
https://www.uml-diagrams.org/state-machine-diagrams.html
https://blog.csdn.net/significantfrank/article/details/104996419
https://segmentfault.com/a/1190000009906317
https://www.jianshu.com/u/c323ec8e077b
文 / 逅弈
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TqXMtS44D4w6d1-KLxcoiQ