FSM-COLA无状态状态机
介绍
什么是状态机
有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。
-- 维基百科
使用场景
针对需要通过状态扭转达到流程控制的场景。例如:收货,上架这两个动作,它们都是基于MQ操作(非顺序消费MQ)。
正常情况下先收货,后上架。但是在特殊情况下,上架消息先下发,收货消息后下发;导致我们先上架,后收货,此刻单据状态为收货状态。
表像是上架消息丢失,产生单据状态逆流。
FSM-cola状态机
FSM-cola状态机是一个无状态状态机,可能理解起来有点矛盾。
那我们先简单的了解一下鼎鼎大名的spring stateMachine,它是一个有状态状态机。使用过spring stateMachine就会发现有很多弊端,其中最大的弊端就是每个stateMachine对象包含了上下文,换句话说就是每个stateMachine都是有状态的。因此为了保证线程安全,所以每次调用spring stateMachine的时候都需要我们去new一个stateMachine对象,这个过程是非常没有必要的,而且还消耗内存。
至此无状态理解起来就很简单了,就是有状态的反面。因此,我们可以这样去做,达到无状态的效果:只要它不包含上下文,只是作为参数在方法中传递,纯粹的方法栈调用。
其实对状态机本身而言,它就只是管理状态的一个工具,或者称它为DSL的一种实现。它能够帮助我们更好对单据状态进行扭转。既然如此,有状态和无状态并不是状态机必备属性。所以是不是可以将状态机做成无状态,这样就先天解决了有状态带来的不必要的内存消耗。基于这种情况,我们基于cola stateMachine状态机开发了属于我们自己的状态机FSM-cola状态机。
说明:
这里特别解释一下为什么说状态机是DSL一种实现。因为要使用状态机,必须要遵循它规定的顺序调用方法,不能随意更改顺序。如同XML那样,每个配置项都是要符合一定的结构顺序才行。只有配置正确了,它的配置项才会生效。
cola stateMachin是阿里大神开发的无状态状态机,且已经被阿里在生产环境中做了验证。但是它还有很多场景是无法实现的,需要结合我们自己的业务做二次开发。
这里重申一下,不是说spring stateMachine 不好,反而是它太好了,好的有点过分。spring stateMachine中有很多优秀的功能,然而这些功能在我们日常开发中,是根本用不到的。导致了spring stateMachine显得太过笨重。
FSM-cola优势
代码简单,易于学习,便于结合我们自己的业务做二次开发。
无状态,内存消耗少,线程先天安全。
先天支持事务。
先天会抛出异常,不会出现像spring stateMachine那样将异常吃掉的情况(当然通过反射确实可以将异常抛出)。
包小减少资源浪费,摒弃了spring stateMachine中没有必要的功能。
FSM-cola状态机基础模型
状态机基础模型
State:状态
Event:事件,状态由事件触发,引起变化
Transition:流转,表示从一个状态到另一个状态
External Transition:外部流转,两个不同状态之间的流转
Internal Transition:内部流转,同一个状态之间的流转(不推荐使用,这种情况下会出现状态循环)
Condition:条件,表示是否允许到达某个状态
Action:动作,到达某个状态之后,可以做什么
StateMachine:状态机
工作流程
支持类型:
INTERNAL --- 内部流转,
LOCAL --- 无具体实现,
EXTERNAL --- 外部流转,
CHOOSE ---自定义新增,
分析和使用
源码分析
源代码
入口:
public class StateMachineBuilderFactory {
public static <S, E, C> StateMachineBuilder<S, E, C> create(){
return new StateMachineBuilderImpl<>();
}
}
public class StateMachineBuilderImpl<S, E, C> implements StateMachineBuilder<S, E, C> {
/**
* StateMap is the same with stateMachine, as the core of state machine is holding reference to states.
* 状态枚举持有对状态机的引用
*/
private final Map<S, State< S, E, C>> stateMap = new ConcurrentHashMap<>();
/**
* 当前状态机实例对象
*/
private final StateMachineImpl<S, E, C> stateMachine = new StateMachineImpl<>(stateMap);
/**
* 一对一: 外循环
* @return ExternalTransitionBuilder
*/
@Override
public ExternalTransitionBuilder<S, E, C> externalTransition() {
return new TransitionBuilderImpl<>(stateMap, TransitionType.EXTERNAL);
}
/**
* 多起始状态对一个终态: 外循环
* @return ExternalTransitionsBuilder
*/
@Override
public ExternalTransitionsBuilder<S, E, C> externalTransitions() {
return new TransitionsBuilderImpl<>(stateMap, TransitionType.EXTERNAL);
}
/**
* 起始状态等于最终状态:内循环
* @return
*/
@Override
public InternalTransitionBuilder<S, E, C> internalTransition() {
return new TransitionBuilderImpl<>(stateMap, TransitionType.INTERNAL);
}
@Override
public StateMachine<S, E, C> build(String machineId) {
stateMachine.setMachineId(machineId);
stateMachine.setReady(true);
StateMachineFactory.register(stateMachine);
return stateMachine;
}
}
public class StateMachineFactory {
/**
* 状态机工厂,来储存所有的状态,key:状态机唯一标识,value状态机
*/
static Map<String /* machineId */, StateMachine> stateMachineMap = new ConcurrentHashMap<>();
/**
* 注册状态机
*
* @param stateMachine
* @param <S>
* @param <E>
* @param <C>
*/
public static <S, E, C> void register(StateMachine<S, E, C> stateMachine) {
String machineId = stateMachine.getMachineId();
if (stateMachineMap.get(machineId) != null) {
throw new StateMachineException("The state machine with id [" + machineId + "] is already built, no need to build again");
}
stateMachineMap.put(stateMachine.getMachineId(), stateMachine);
}
......
}
cola核心就是两个MAP
第一个MAP: StateMachineFactory中的stateMachineMap
/**
* 状态机工厂,来储存所有的状态,key:状态机唯一标识,value状态机
*/
static Map<String /* machineId */, StateMachine> stateMachineMap = new ConcurrentHashMap<>();
key:machineId是状态机唯一标识,这个标识具体表现,举一个例子。
比如OFC项目,OFC项目是通过控制单据状态来达到流程控制。OFC项目中分为:出库流程,入库流程。
这两个流程(也是两个不同作用域)。入库流程中包含了:创建,收货,上架等等行为。出库流程包含了:创建,分配库存,拣货下架,发货出库等等行为。
这两个流程是完全不同的作用域,它们分别作用在入库单上,和出库单上。因此建议使用两个不同的状态机来做区分,即两个不同的machineId。
value: StateMachine是一个状态机对象,这个对象可以粗浅的认为是:一个作用域(比如:入库流程)。
第二个MAP: StateMachineBuilderImpl中的stateMap
/**
* StateMap is the same with stateMachine, as the core of state machine is holding reference to states.
* 状态枚举持有对状态机的引用,key: 起始状态,value: State
*/
private final Map<S, State< S, E, C>> stateMap = new ConcurrentHashMap<>()
key:起始状态枚举
state:起始状态对应的状态对象
DSL语法实现:
目前提供了2种实现: TransitionBuilderImpl,TransitionsBuilderImpl
class TransitionBuilderImpl<S, E, C> implements ExternalTransitionBuilder<S, E, C>, InternalTransitionBuilder<S, E, C>, From<S, E, C>, On<S, E, C>, To<S, E, C> {
/**
* 状态机集合
*/
final Map<S, State<S, E, C>> stateMap;
/**
* 源状态
*/
private State<S, E, C> source;
/**
* 目标状态
*/
protected State<S, E, C> target;
/**
* 状态扭转实体
*/
private Transition<S, E, C> transition;
/**
* 状态扭转类型
*/
final TransitionType transitionType;
public TransitionBuilderImpl(Map<S, State<S, E, C>> stateMap, TransitionType transitionType) {
this.stateMap = stateMap;
this.transitionType = transitionType;
}
/**
* 起始状态
* @param stateId id of state
* @return
*/
@Override
public From<S, E, C> from(S stateId) {
source = StateHelper.getState(stateMap, stateId);
return this;
}
/**
* 目标状态
* @param stateId id of state
* @return
*/
@Override
public To<S, E, C> to(S stateId) {
target = StateHelper.getState(stateMap, stateId);
return this;
}
/**
* 起始状态等于目标状态
* @param stateId id of transition
* @return
*/
@Override
public To<S, E, C> within(S stateId) {
source = target = StateHelper.getState(stateMap, stateId);
return this;
}
/**
* 判断条件
* @param condition transition condition
* @return
*/
@Override
public When<S, E, C> when(Condition<C> condition) {
transition.setCondition(condition);
return this;
}
/**
* 执行的事件
* @param event transition event
* @return
*/
@Override
public On<S, E, C> on(E event) {
transition = source.addTransition(event, target, transitionType);
return this;
}
/**
* 需要执行的动作
* @param action performed action
*/
@Override
public void perform(Action<S, E, C> action) {
transition.setAction(action);
}
}
TransitionsBuilderImpl 与TransitionBuilderImpl不同的是多了一层循环。
@Override
public From<S, E, C> fromAmong(S... stateIds) {
for(S stateId : stateIds) {
sources.add(StateHelper.getState(super.stateMap, stateId));
}
return this;
}
@Override
public On<S, E, C> on(E event) {
for(State source : sources) {
Transition transition = source.addTransition(event, super.target, super.transitionType);
transitions.add(transition);
}
return this;
}
......
StateHelper.getState()
public class StateHelper {
public static <S, E, C> State<S, E, C> getState(Map<S, State<S, E, C>> stateMap, S stateId){
State<S, E, C> state = stateMap.get(stateId);
if (state == null) {
//初始化state对象
state = new StateImpl<>(stateId);
stateMap.put(stateId, state);
}
return state;
}
}
就是赋值,将状态赋值到stateMap中。
关键的方法是:source.addTransition(event, target, transitionType)
它属于State一个实现类。
public class StateImpl<S, E, C> implements State<S, E, C> {
protected final S stateId;
private HashMap<E, Transition<S, E, C>> transitions = new HashMap<>();
public StateImpl(S stateId) {
this.stateId = stateId;
}
@Override
public Transition<S, E, C> addTransition(E event, State<S, E, C> target, TransitionType transitionType) {
Transition<S, E, C> newTransition = new TransitionImpl<>();
newTransition.setSource(this);
newTransition.setTarget(target);
newTransition.setEvent(event);
newTransition.setType(transitionType);
Debugger.debug("Begin to add new transition: " + newTransition);
verify(event, newTransition);
transitions.put(event, newTransition);
return newTransition;
}
/**
* Per one source and target state, there is only one transition is allowed
*
* @param event
* @param newTransition
*/
private void verify(E event, Transition<S, E, C> newTransition) {
Transition existingTransition = transitions.get(event);
if (existingTransition != null) {
if (existingTransition.equals(newTransition)) {
throw new StateMachineException(existingTransition + " already Exist, you can not add another one");
}
}
}
......
}
核心实现:
Transition其实现类:TransitionImpl核心代码。
private State<S, E, C> source;
private State<S, E, C> target;
private E event;
private Condition<C> condition;
private Action<S, E, C> action;
private TransitionType type = TransitionType.EXTERNAL;
......
@Override
public State<S, E, C> transit(C ctx) {
Debugger.debug("Do transition: " + this);
this.verify();
if (condition == null || condition.isSatisfied(ctx)) {
if (action != null) {
action.execute(source.getId(), target.getId(), event, ctx);
}
return target;
}
Debugger.debug("Condition is not satisfied, stay at the " + source + " state ");
return source;
}
到这里已经完成了大致代码实现。
源代码
到此可以得出一个基本状态机结构:
使用
internalTransition
@Test
public void testInternalNormal() {
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.internalTransition()
.within(States.STATE1)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID + "2");
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
Assert.assertEquals(States.STATE1, target);
}
这种方式不推荐使用,原因是这种情况下会出现状态循环。
应用场景:修改TMS在途运单的节点时间。运单在途过程中有多个阶段需要更新时间。且对于在途运单修改节点事件时,这个在途运单都是同一个状态。(个人认为可以定义多个状态其实也是可以满足,不一定只用一个状态来确保)。
externalTransitions
@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);
}
这种场景是:取消场景,取消场景中,起始状态有很多比如发货,上架等等都是可以取消的。
externalTransition
@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);
}
这个场景是最常见的场景:
比如:发货->上架。
chooseExternalTransitions
@Test
public void testChooseExternalNormal() {
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.chooseExternalTransitions()
.from(States.STATE1)
.on(Events.EVENT1)
.caseThen(checkCondition(false), States.STATE2, doAction())
.caseThen(checkCondition(true), States.STATE3, doAction())
.end(States.STATE4, doAction());
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context(1));
Assert.assertEquals(States.STATE2, target);
}
场景:
创建(正向,逆向)出库单,创建可分为:(正向)销售出库单创建,(逆向)退货出库单创建。
自定义开发
背景
在使用cola stateMachine的时候,有一种业务场景是当前状态机无法实现的,就是当 当前状态加事件是同一个,但是目标状态不一样的时候。
比如:退货出库(逆向),销售出库(正向)创建出库单的时候,它们的事件都是 createBillEvent 。但是由于业务流程不同,导致了目标状态是 (正向)待出库 和 (逆向)待下架。
当然你可以通过定义不同事件来处理这种场景。比如定义:退货出库事件,销售出库事件,不使用相同的事件。或者在业务代码上使用if/else 判断处理。
但是个人认为这是一种折中方案。我认为cola stateMachine将checkCondition 方法的能力弱化了。checkCondition在cola stateMachine中只是做了if 判断。 现实场景中有很多 if/else-if/else判断。因此在自定义开发中,我就加强了这个。为了达到这种判断,我新增一种类型 CHOOSE,这种类型是外部状态扭转的一种实现。
public class ChooseTransitionBuilderImpl<S, E, C> implements ChooseExternalTransitionBuilder<S, E, C>, ChooseFrom<S, E, C>, ChooseOn<S, E, C> {
/**
* 状态机集合
*/
final Map<S, State<S, E, C>> stateMap;
/**
* 源状态
*/
private State<S, E, C> source;
/**
* 目标状态
*/
protected State<S, E, C> target;
/**
* 状态扭转实体
*/
private Transition<S, E, C> transition;
/**
* 状态扭转类型
*/
final TransitionType transitionType;
private E event;
public ChooseTransitionBuilderImpl(Map<S, State<S, E, C>> stateMap, TransitionType transitionType) {
this.stateMap = stateMap;
this.transitionType = transitionType;
}
@Override
public ChooseFrom<S, E, C> from(S stateId) {
this.source = StateHelper.getState(stateMap, stateId);
return this;
}
@Override
public ChooseOn<S, E, C> on(E event) {
this.event = event;
return this;
}
@Override
public ChooseOn<S, E, C> caseThen(Condition<C> condition, S stateId, Action<S, E, C> action) {
target = StateHelper.getState(stateMap, stateId);
transition = source.addTransitionByChoose(event, target, TransitionType.CHOOSE);
transition.setAction(action);
transition.setCondition(condition);
return this;
}
@Override
public void end(S stateId, Action<S, E, C> action) {
target = StateHelper.getState(stateMap, stateId);
transition = source.addTransitionByChoose(event, target, TransitionType.CHOOSE);
transition.setAction(action);
transition.setCondition(context -> true);
}
}
@Override
public Transition<S, E, C> addTransitionByChoose(E event, State<S, E, C> target, TransitionType transitionType) {
verifyChoose(event);
Transition<S, E, C> newTransition = new TransitionImpl<>();
newTransition.setSource(this);
newTransition.setTarget(target);
newTransition.setEvent(event);
newTransition.setType(transitionType);
Debugger.debug("Begin to add Choose Transition: " + newTransition);
List<Transition<S, E, C>> transitions;
if (chooseTransitions.containsKey(event)) {
transitions = chooseTransitions.get(event);
} else {
transitions = new ArrayList<>();
}
transitions.add(newTransition);
eventHashMap.put(event,transitionType);
chooseTransitions.put(event, transitions);
return newTransition;
}
使用
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.chooseExternalTransitions()
.from(States.STATE1)
.on(Events.EVENT1)
.caseThen(checkCondition(false), States.STATE1, doAction())
.caseThen(checkCondition(true), States.STATE3, doAction())
.end(States.STATE4, doAction());
扩展
支持分布式
状态机可以保证单据状态无法逆流,但是只限定到了单台服务器上,且无法做到防并发。如果在分布式场景下,多台机器对同一个单据处理,就无能为力了。因此分布式锁是必须的。
状态机入参封装
原生状态机,入参是Object。这样就存在一个问题,一万个人有一万种方式,代码维护困难,可读性差。而且无法获取状态机当前状态等等常用的参数信息。因此需要统一入参对象,提高代码可读性。
状态机通用模板支持
状态机使用场景中,除了统一入参以外,还有一些流程是相同的,比如:幂等,状态判断。出于提高代码复用性的考虑,决定使用一个模板方法。
注解支持
@RequestExt:防并发扩展注解
@RequestId:防并发注解
除了参数幂等以外,还支持注解的形式。在当前状态机扩展类中,幂等key是一定要保证的,为了防止重复请求。
总结一下,一个完整状态机,必须满足一下几点:
状态机必须支持分布式。
状态机必须保证状态控制。
状态机必须保证线程安全。
参考链接
https://blog.csdn.net/significantfrank/article/details/104996419
关注得物技术,携手走向技术的云端
文|zero