共计 12692 个字符,预计需要花费 32 分钟才能阅读完成。
一、业务背景
营销自动化平台反对多种不同类型经营流动策略(比方:短信推送策略、微信图文推送策略、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 营销自动化业务拓展性|引擎篇 01》
前面咱们将持续带来系列专题文章的其余内容,每一篇文章都会对外面的技术实际进行详尽解析,敬请期待。
作者:vivo 互联网服务器团队 -Chen Wangrong