一、業務背景
營銷自動化平臺支援多種不同型別運營活動策略(比如:簡訊推送策略、微信圖文推送策略、App Push推送策略),每種活動型別都有各自不同的執行流程和活動狀態。比如簡訊活動的活動執行流程如下:
(圖1-1:簡訊活動狀態轉移)
整個簡訊活動經歷了 未開始 → 資料準備中 → 資料已就緒 → 活動推送中→ 活動結束 多個狀態變更流程。不僅如此, 我們發現在活動業務邏輯處理過程中,都有以下類似的特點:
-
每增加一種新的活動業務型別,就要新增相應的活動狀態以及處理狀態變更的邏輯;
-
當一個型別的活動業務流程有修改時,可能需要對原先的狀態轉移過程進行變更;
-
當每個業務都各自編寫自己的狀態轉移的業務程式碼時,核心業務邏輯和控制邏輯耦合性會非常強,擴充套件性差,成本也很高。
針對系統狀態的流轉管理,計算機領域有一套標準的理論方案模型——有限狀態機。
二、理解狀態機
2.1 狀態機定義
有限狀態機(Finite-State Machine , 縮寫:FSM),簡稱狀態機。是表示有限個狀態以及這些狀態之間的轉移和觸發動作的模型。
-
狀態是描述系統物件在某個時刻所處的狀況。
-
轉移指示狀態變更,一般是通過外部事件為條件觸發狀態的轉移。
-
動作是對給定狀態下要進行的操作。
簡而言之,狀態機是由事件、狀態、動作三大部分組成。三者的關係是:事件觸發狀態的轉移,狀態的轉移觸發後續動作的執行。其中動作不是必須的,也可以只進行狀態轉移,不進行任何操作。
(圖2-1:狀態機組成)
所以將上述【圖1-1:簡訊活動狀態轉移 】使用狀態機模型來描述就是:
(圖2-2:簡訊活動狀態機)
狀態機本質上是對系統的一種數學建模,將問題解決方案系統化表達出來。下面我們來看下在實際開發中有哪些實現狀態機的方式 。
2.2 狀態機的實現方式
2.2.1 基於條件判斷的實現
這是最直接的一種實現方式,所謂條件判斷就是通過使用 if-else 或 switch-case 分支判斷進行硬編碼實現。對於前面簡訊活動,基於條件判斷方式的程式碼例項如下:
/**
* 簡訊活動狀態列舉
*/
public enum ActivityState {
NOT_START(0), //活動未開始
DATA_PREPARING(1), //資料準備中
DATA_PREPARED(2), //資料已就緒
DATA_PUSHING(3), //活動推送中
FINISHED(4); //活動結束
}
/**
* 簡訊活動狀態機
*/
public class ActivityStateMachine {
//活動狀態
private ActivityState currentState;
public ActivityStateMachine() {
this.currentState = ActivityState.NOT_START;
}
/**
* 活動時間開始
*/
public void begin() {
if (currentState.equals(ActivityState.NOT_START)) {
this.currentState = ActivityState.DATA_PREPARING;
//傳送通知給運營人員
notice();
}
// do nothing or throw exception ...
}
/**
* 資料計算完成
*/
public void finishCalData() {
if (currentState.equals(ActivityState.DATA_PREPARING)) {
this.currentState = ActivityState.DATA_PREPARED;
//傳送通知給運營人員
notice();
}
// do nothing or throw exception ...
}
/**
* 活動推送開始
*/
public void beginPushData() {
//省略
}
/**
* 資料推送完成
*/
public void finishPushData() {
//省略
}
}
通過條件分支判斷來控制狀態的轉移和動作的觸發,上述的 if 判斷條件也可以換成 switch 語句,以當前狀態為分支來控制該狀態下可以執行的操作。
適用場景
適用於業務狀態個數少或者狀態間跳轉邏輯比較簡單的場景。
缺陷
當觸發事件和業務狀態之間對應關係不是簡單的一對一時,就需要巢狀多個條件分支判斷,分支邏輯會變得異常複雜;當狀態流程有變更時,也需要改動分支邏輯,不符合開閉原則,程式碼可讀性和擴充套件性非常差。
2.2.2 基於狀態模式的實現
瞭解設計模式的童鞋,很容易就可以把狀態機和狀態模式這兩個概念聯絡起來,狀態模式其實可以作為狀態機的一種實現方式。主要實現思路是通過狀態模式將不同狀態的行為進行分離,根據狀態變數的變化,來呼叫不同狀態下對應的不同方法。程式碼示例如下:
/**
* 活動狀態介面
*/
interface IActivityState {
ActivityState getName();
//觸發事件
void begin();
void finishCalData();
void beginPushData();
void finishPushData();
}
/**
* 具體狀態類—活動未開始狀態
*/
public class ActivityNotStartState implements IActivityState {
private ActivityStateMachine stateMachine;
public ActivityNotStartState(ActivityStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public ActivityState getName() {
return ActivityState.NOT_START;
}
@Override
public void begin() {
stateMachine.setCurrentState(new ActivityDataPreparingState(stateMachine));
//傳送通知
notice();
}
@Override
public void finishCalData() {
// do nothing or throw exception ...
}
@Override
public void beginPushData() {
// do nothing or throw exception ...
}
@Override
public void finishPushData() {
// do nothing or throw exception ...
}
}
/**
* 具體狀態類—資料準備中狀態
*/
public class ActivityDataPreparingState implements IActivityState {
private ActivityStateMachine stateMachine;
public ActivityNotStartState(ActivityStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public ActivityState getName() {
return ActivityState.DATA_PREPARING;
}
@Override
public void begin() {
// do nothing or throw exception ...
}
public void finishCalData() {
stateMachine.setCurrentState(new ActivityDataPreparedState(stateMachine));
//TODO:傳送通知
}
@Override
public void beginPushData() {
// do nothing or throw exception ...
}
@Override
public void finishPushData() {
// do nothing or throw exception ...
}
}
...(篇幅原因,省略其他具體活動類)
/**
* 狀態機
*/
public class ActivityStateMachine {
private IActivityState currentState;
public ActivityStateMachine(IActivityState currentState) {
this.currentState = new ActivityNotStartState(this);
}
public void setCurrentState(IActivityState currentState) {
this.currentState = currentState;
}
public void begin() {
currentState.begin();
}
public void finishCalData() {
currentState.finishCalData();
}
public void beginPushData() {
currentState.beginPushData();
}
public void finishPushData() {
currentState.finishCalData();
}
}
狀態模式定義了狀態-行為的對應關係, 並將各自狀態的行為封裝在對應的狀態類中。我們只需要擴充套件或者修改具體狀態類就可以實現對應流程狀態的需求。
適用場景
適用於業務狀態不多且狀態轉移簡單的場景,相比於前面的if/switch條件分支法,當業務狀態流程新增或修改時,影響粒度更小,範圍可控,擴充套件性更強。
缺陷
同樣難以應對業務流程狀態轉移複雜的場景,此場景下使用狀態模式會引入非常多的狀態類和方法,當狀態邏輯有變更時,程式碼也會變得難以維護。
可以看到,雖然以上兩種方式都可以實現狀態機的觸發、轉移、動作流程,但是複用性都很低。如果想要構建一個可以滿足絕大部分業務場景的抽象狀態機元件,是無法滿足的。
2.2.3 基於DSL的實現
2.2.3.1 DSL 介紹
DSL 全稱是 Domain-Specific Languages,指的是針對某一特定領域,具有受限表達性的一種計算機程式設計語言。不同於通用的程式語言,DSL只用在某些特定的領域,聚焦於解決該領域系統的某塊問題。DSL通常分為 內部 DSL ( Internal DSLs ),外部 DSL ( external DSLs ) 。
-
內部DSL :基於系統的宿主語言,由宿主語言進行編寫和處理的 DSL,比如:基於 Java 的 內部 DSL 、基於 C++ 的內部 DSL 、基於 Javascript 的 內部 DSL 。
-
外部DSL :不同於系統宿主語言,由自定義語言或者其他程式語言編寫並處理的 DSL,有獨立的解析器。比如:正規表示式、XML、SQL、HTML 等。
(有關DSL的更多內容可以瞭解:Martin Fowler《Domain Specific Languages》)。
2.2.3.2 DSL 的選型和狀態機實現
使用DSL作為開發工具,可以用更加清晰和更具表達性的形式來描述系統的行為。DSL 也是目前實現狀態機比較推薦的方式,可以根據自身的需要選用內部 DSL 或者外部DSL 來實現。
- 內部 DSL :業務系統如果只希望通過程式碼直接進行狀態機的配置,那麼可以選擇使用內部 DSL,特點是簡單直接,不需要依賴額外的解析器和元件。
Java 內部 DSL 一般是利用 Builder Pattern 和 Fluent Interface 方式(Builder 模式和流式介面),實現示例:
StateMachineBuilder builder = new StateMachineBuilder();
builder.sourceState(States.STATE1)
.targetState(States.STATE2)
.event(Events.EVENT1)
.action(action1());
- 外部 DSL :可以利用外部儲存和通用指令碼語言的解析能力,實現執行時動態配置、支援視覺化配置和跨語言應用場景。
外部 DSL 本質上就是將狀態轉移過程用其他外部語言進行描述,比如使用 XML 的方式:
<state id= "STATE1">
<transition event="EVENT1" target="STATE2">
<action method="action1()"/>
</transition>
</state>
<state id= "STATE2">
</state>
外部 DSL 一般放在配置檔案或者資料庫等外部儲存中,經過對應的文字解析器,就可以將外部 DSL 的配置解析成類似內部 DSL 的模型,進行流程處理;同時由於外部儲存的獨立性和永續性,可以很方便地支援執行時動態變更和視覺化配置。
Java開源的狀態機框架基本上都是基於DSL的實現方式。
三、開源狀態機框架
我們分別使用三種開源狀態機框架來完成簡訊活動狀態流轉過程。
3.1 Spring Statemachine
enum ActivityState {
NOT_START(0),
DATA_PREPARING(1),
DATA_PREPARED(2),
DATA_PUSHING(3),
FINISHED(4);
private int state;
private ActivityState(int state) {
this.state = state;
}
}
enum ActEvent {
ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
}
@Configuration
@EnableStateMachine
public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<ActivityState, ActEvent> {
@Override
public void configure(StateMachineStateConfigurer<ActivityState, ActEvent> states)
throws Exception {
states
.withStates()
.initial(ActivityState.NOT_START)
.states(EnumSet.allOf(ActivityState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<ActivityState, ActEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(ActivityState.NOT_START).target(ActivityState.DATA_PREPARING)
.event(ActEvent.ACT_BEGIN).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PREPARING).target(ActivityState.DATA_PREPARED)
.event(ActEvent.FINISH_DATA_CAL).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PREPARED).target(ActivityState.DATA_PUSHING)
.event(ActEvent.FINISH_DATA_PREPARE).action(notice())
.and()
.withExternal()
.source(ActivityState.DATA_PUSHING).target(ActivityState.FINISHED)
.event(ActEvent.FINISH_DATA_PUSHING).action(notice())
.and() ;
}
@Override
public void configure(StateMachineConfigurationConfigurer<ActivityState, ActEvent> config)
throws Exception {
config.withConfiguration()
.machineId("ActivityStateMachine");
}
public Action<ActivityState, ActEvent> notice() {
return context -> System.out.println("【變更前狀態】:"+context.getSource().getId()+";【變更後狀態】:"+context.getTarget().getId());
}
//測試類
class DemoApplicationTests {
@Autowired
private StateMachine<ActivityState, ActEvent> stateMachine;
@Test
void contextLoads() {
stateMachine.start();
stateMachine.sendEvent(ActEvent.ACT_BEGIN);
stateMachine.sendEvent(ActEvent.FINISH_DATA_CAL);
stateMachine.sendEvent(ActEvent.FINISH_DATA_PREPARE);
stateMachine.sendEvent(ActEvent.FINISH_DATA_PUSHING);
stateMachine.stop();
}
}
通過重寫配置模板類的三個configure方法,通過流式Api形式完成狀態初始化、狀態轉移的流程以及狀態機的宣告,實現Java內部DSL的狀態機。外部使用狀態機通過sendEvent事件觸發,推動狀態機的自動流轉。
優勢
-
Spring Statemachine 是 Spring 官方的產品,具有強大生態社群。
-
功能十分完備,除了支援基本的狀態機配置外,還具備可巢狀的子狀態機、基於zk的分散式狀態機和外部儲存持久化等豐富的功能特性。
缺陷
-
Spring Statemachine 在每個 statemachine 例項內部儲存了當前狀態機上下文相關的屬性,也就是說是有狀態的(這一點從觸發狀態機流轉只需事件作為引數也可以看出來),所以使用單例模式的狀態機例項不是執行緒安全的。要保證執行緒安全性只能每次通過工廠模式建立一個新的狀態機例項,這種方式在高併發場景下,會影響系統整體效能。
-
程式碼層次結構稍顯複雜,二次開發改造成本大,一般場景下也並不需要使用如此多的功能,使用時觀感上顯得比較沉重。
3.2 Squirrel Foundation
public class SmsStatemachineSample {
// 1. 狀態定義
enum ActivityState {
NOT_START(0),
DATA_PREPARING(1),
DATA_PREPARED(2),
DATA_PUSHING(3),
FINISHED(4);
private int state;
private ActivityState(int state) {
this.state = state;
}
}
// 2. 事件定義
enum ActEvent {
ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
}
// 3. 狀態機上下文
class StatemachineContext {
}
@StateMachineParameters(stateType=ActivityState.class, eventType=ActEvent.class, contextType=StatemachineContext.class)
static class SmsStatemachine extends AbstractUntypedStateMachine {
protected void notice(ActivityState from, ActivityState to, ActEvent event, StatemachineContext context) {
System.out.println("【變更前狀態】:"+from+";【變更後狀態】:"+to);
}
}
public static void main(String[] args) {
// 4. 構建狀態轉移
UntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(SmsStatemachine.class);
builder.externalTransition().from(ActivityState.NOT_START).to(ActivityState.DATA_PREPARING).on(ActEvent.ACT_BEGIN).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PREPARING).to(ActivityState.DATA_PREPARED).on(ActEvent.FINISH_DATA_CAL).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PREPARED).to(ActivityState.DATA_PUSHING).on(ActEvent.FINISH_DATA_PREPARE).callMethod("notice");
builder.externalTransition().from(ActivityState.DATA_PUSHING).to(ActivityState.FINISHED).on(ActEvent.FINISH_DATA_PUSHING).callMethod("notice");
// 5. 觸發狀態機流轉
UntypedStateMachine fsm = builder.newStateMachine(ActivityState.NOT_START);
fsm.fire(ActEvent.ACT_BEGIN, null);
fsm.fire(ActEvent.FINISH_DATA_CAL, null);
fsm.fire(ActEvent.FINISH_DATA_PREPARE, null);
fsm.fire(ActEvent.FINISH_DATA_PUSHING, null);
}
}
squirrel-foundation 是一款輕量級的狀態機庫,設計目標是為企業使用提供輕量級、高度靈活、可擴充套件、易於使用、型別安全和可程式設計的狀態機實現。
優勢
-
和目標理念一致,與 Spring Statemachine 相比,不依賴於spring框架,設計實現方面更加輕量,雖然也是有狀態的設計,但是建立狀態機例項開銷較小,功能上也更加簡潔,相對比較適合二次開發。
-
對應的文件和測試用例也比較豐富,開發者上手容易。
缺陷
-
過於強調“約定優於配置”的理念,不少預設性的處理,比如狀態轉移後動作是通過方法名來呼叫,不利於操作管理。
-
社群活躍度不高。
3.3 Cola Statemachine
/**
* 狀態機工廠類
*/
public class StatusMachineEngine {
private StatusMachineEngine() {
}
private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap();
static {
//簡訊推送狀態
STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
//PUSH推送狀態
STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
//......
}
public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
return STATUS_MACHINE_MAP.get(channelTypeEnum);
}
/**
* 觸發狀態轉移
* @param channelTypeEnum
* @param status 當前狀態
* @param eventType 觸發事件
* @param context 上下文引數
*/
public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
//推動狀態機進行流轉,具體介紹本期先省略
orderStateMachine.fireEvent(status, eventType, context);
}
/**
* 簡訊推送活動狀態機初始化
*/
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private StatusAction smsStatusAction;
@Autowired
private StatusCondition smsStatusCondition;
//基於DSL構建狀態配置,觸發事件轉移和後續的動作
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(INIT)
.to(NOT_START)
.on(EventType.TIME_BEGIN)
.when(smsStatusAction.checkNotifyCondition())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(NOT_START)
.to(DATA_PREPARING)
.on(EventType.CAL_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(DATA_PREPARING)
.to(DATA_PREPARED)
.on(EventType.PREPARED_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
...(省略其他狀態)
builder.build(StatusMachineEngine.getMachineEngine(ChannelTypeEnum.SMS));
}
//呼叫端
public class Client {
public static void main(String[] args){
//構建活動上下文
Context context = new Context(...);
// 觸發狀態流轉
StatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context);
}
}
}
Cola Statemachine 是阿里COLA開源框架裡面的一款狀態機框架,和前面兩者最大的不同就是:無狀態的設計——觸發狀態機流轉時需要把當前狀態作為入參,狀態機例項中不需要保留當前狀態上下文訊息,只有一個狀態機例項,也就直接保證了執行緒安全性和高效能。
優勢
-
輕量級無狀態,安全,效能高。
-
設計簡潔,方便擴充套件。
-
社群活躍度較高。
缺陷
- 不支援巢狀、並行等高階功能。
3.4 小結
三種開源狀態機框架對比如下:
希望直接利用開源狀態機能力的系統,可以根據自身業務的需求和流程複雜度,進行合適的選型。
四、營銷自動化業務案例實踐
4.1 設計選型
vivo營銷自動化的業務特點是:
-
運營活動型別多,業務流量大,流程相對簡單,效能要求高。
-
流程變更頻繁,經常需要新增業務狀態,需要支援快速新增配置和變更。
-
在狀態觸發後會有多種不同的業務操作,比如狀態變更後的訊息提醒,狀態完結後的業務處理等,需要支援非同步操作和方便擴充套件。
針對以上業務特點,在實際專案開發中,我們是基於開源狀態的實現方案——基於內部DSL的方式進行開發。同時汲取了以上開源框架的特點,選用了無狀態高效能、功能簡潔、支援動作非同步執行的輕量設計。
-
無狀態高效能:保證高效能,採用無狀態的狀態機設計,只需要一個狀態機例項就可以進行運轉。
-
功能簡潔:最小設計原則,只保留核心的設計,比如事件觸發,狀態的基本流轉,後續的操作和上下文引數處理。
-
動作非同步執行:針對非同步業務流程,採用執行緒池或者訊息佇列的方式進行非同步解耦。
4.2 核心流程
-
沿用開源狀態機的內部DSL流式介面設計,在應用啟動時掃描狀態機定義;
-
建立非同步處理執行緒池支援業務的後置動作;
-
解析狀態機的DSL配置,初始化狀態機例項;
-
構建執行上下文,存放各個狀態機的例項和其他執行過程資訊;
-
狀態機觸發時,根據觸發條件和當前狀態,自動匹配轉移過程,推動狀態機流轉;
-
執行後置同步/非同步處理操作。
(圖4-1:核心流程設計)
4.3 實踐思考
1)狀態機配置視覺化,結合外部DSL的方式(比如JSON的方式,儲存到資料庫中),支援更快速的配置。
2)目前只支援狀態的簡單流轉,在流轉過程加入流轉介面擴充套件點,應對未來可能出現的複雜場景。
五、總結
狀態機是由事件、狀態、動作三大部分組成。三者的關係是:事件觸發狀態的轉移,狀態的轉移觸發後續動作的執行。利用狀態機進行系統狀態管理,可以提升業務擴充套件性和內聚性。狀態機可以使用條件分支判斷、狀態模式和基於DSL來實現,其中更具表達性的DSL也是很多開源狀態機的實現方式。可以基於開源狀態機的特點和自身專案需求進行合適的選型,也可以基於前面的方案自定義狀態機元件。
本篇是《營銷自動化技術解密》系列專題文章的第三篇,系列文章回顧:
後面我們將繼續帶來系列專題文章的其他內容,每一篇文章都會對裡面的技術實踐進行詳盡解析,敬請期待。
作者:vivo網際網路伺服器團隊-Chen Wangrong