有限自动状态机 (Finite-state machine , FSM) 通常用来描述某个具有有限个状态的对象,并且在对象的生命周期中组成了一个状态序列,通过响应外界各种事件完成状态流转。
FSM 被广泛应用于 建模应用行为,硬件电路系统设计,软件工程,编译器,网络协议和计算机语言的研究。
FSM 应用场景满足的规则:
落地的应用场景:
cola-component-statemachine (java)
squirrel-foundation (java)
Spring statemachine (java)
我们本节将基于Looplab fsm (go) 进行改造,改造点主要有以下几个:
同一个event下,一个现态 , 可流转到不同的次态
传统概念的状态机中,一个src和一个event的组合,只能确定一个且仅有一个的dst,但是经过改造后,一个src和一个event的组合,可能会关联多个dst,这样做并不是改变了状态机的模型,而是通过将相似的event合并,配合条件表达式,也就是组成src,event , 和条件表达式的三元组,唯一的确定可流转的dst。这样做的好处有两点:
以下单场景为例:
订单处于 “下单” 状态,当接收到 “创建订单” 事件时。根据订单类型的不同可以分为0元单和非0元单,传统的FSM会将两种类型的订单创建定义为两个不同的event : “创建0元订单” 和 “创建非0元订单” ,但是在bfsm中,可以只定义一个 “创建订单” 的 event ,配合条件表达式判断订单类型,将状态流转到不同的dst 。这样可以简化配置,同时也不需要将 “创建订单” 这个event做更细粒度的拆解。
匹配表达式
根据src 和 event ,能够匹配到一组 dst ,通过匹配表达式执行复杂匹配逻辑,每个匹配条件被满足后对应一个dst,在状态流转的过程中,会按照表达式的注册顺序依次进行匹配,最终会匹配执行结果为true的表达式所对应的dst ;如果所有匹配表达式执行结果都为false,那么状态不会发生流转。
可合并多场景的状态转移配置
可以将多个场景的状态转移配置合并,不合并也可以正常使用。
加锁状态流转
为应对高并发场景,支持基于redis分布式锁的状态转移,对状态转移,通过锁定状态转移的实体对象(通常为订单id,服务单id等),锁定事件fire过程,保证高并发场景下,同一实体对象的状态流程串行执行。另外,支持用户自定义锁的实现。
多对多状态配置
简化配置,提供多状态到多状态的流转配置。
状态配置的图化
基于状态流转配置,在线展示状态转移图。
Looplab fsm 一个简单的使用示例如下所示:
func main() {
var afterFinishCalled bool
fsm := fsm.NewFSM(
// 初态
"start",
// 状态流转图
fsm.Events{
// 事件名 / 现态 / 次态
// 现态 + 事件 = 次态
{Name: "run", Src: []string{"start"}, Dst: "end"},
{Name: "finish", Src: []string{"end"}, Dst: "finished"},
{Name: "reset", Src: []string{"end", "finished"}, Dst: "start"},
},
// 回调接口集合
fsm.Callbacks{
// 在进入end状态前,会回调该接口
"enter_end": func(ctx context.Context, e *fsm.Event) {
if err := e.FSM.Event(ctx, "finish"); err != nil {
fmt.Println(err)
}
},
// 再离开finish状态时,会回调该接口
"after_finish": func(ctx context.Context, e *fsm.Event) {
afterFinishCalled = true
if e.Src != "end" {
panic(fmt.Sprintf("source should have been 'end' but was '%s'", e.Src))
}
if err := e.FSM.Event(ctx, "reset"); err != nil {
fmt.Println(err)
}
},
},
)
// 触发run事件
if err := fsm.Event(context.Background(), "run"); err != nil {
panic(fmt.Sprintf("Error encountered when triggering the run event: %v", err))
}
if !afterFinishCalled {
panic(fmt.Sprintf("After finish callback should have run, current state: '%s'", fsm.Current()))
}
// 查看当前状态
currentState := fsm.Current()
if currentState != "start" {
panic(fmt.Sprintf("expected state to be 'start', was '%s'", currentState))
}
fmt.Println("Successfully ran state machine.")
}
Looplab fsm 只支持 event ,state 二元组状态流转方式,所以整理实现流程比较简单,如下图所示:
Looplab fsm 核心代码都位于 fsm.go 文件中,具体实现大家可以去阅读该源文件进行学习。
改造的具体代码实现此处就不贴出来了,只给出流程图级别的改造说明:
加锁:
异常处理:
表达式:
多场景状态转移配置合并:
每种场景下的配置伪代码如下:
FSMConf := map[string]FsmDesc{
"场景名1" : {
// 当前场景下的全局回调接口
BeforeTransCallback
AfterTransCallback
// 支持多状态到多状态流转
TransDesc: []TransDesc{
{
// 事件名
EventName
// 现态集合
Src []string{}
// 属于本次状态流转过程中的局部回调接口
BeforeTransCallback
AfterTransCallback
// 表达式集合
Matchers []Matcher{
// 表达式,次态,回调接口
{Condition,Dst,BeforeMatchCallback,AfterMatchCallback}
}
}
}
},
"场景2" : {}
}
FSM 初始化过程也分为了两步:
fsm.Init(FSMConf)
fsm.NewFSM("场景名","初态")