自己动手造一个状态机

自己动手造一个状态机

  • 引言
  • 有限自动状态机 (FSM)
    • 五要素
    • 应用场景
    • 优势
  • 开源产品
  • 造个轮子
    • 改造点
    • Looplab fsm
      • 示例演示
      • 实现解析
    • 改造过程


引言

有限自动状态机 (Finite-state machine , FSM) 通常用来描述某个具有有限个状态的对象,并且在对象的生命周期中组成了一个状态序列,通过响应外界各种事件完成状态流转。

FSM 被广泛应用于 建模应用行为,硬件电路系统设计,软件工程,编译器,网络协议和计算机语言的研究。


有限自动状态机 (FSM)

五要素

  • 现态 (src state) : 事物当前所处的状态
  • 事件 (event) : 事件就是执行某个操作后触发的条件或者口令,当一个事件被满足,将会触发一个动作,或者执行一次状态的迁移。
  • 行为 (action) : 事件满足后执行的动作,动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当事件满足后,也可以不执行任何动作,直接迁移到新状态。
  • 次态 (dst state) : 事件满足后要迁往的新状态,‘次态’是相对于‘现态’而言的,‘次态’一旦被激活,就转变成新的‘现态’了。
  • 状态流转 (transition) : 事物从现态变为次态的整个过程。

应用场景

FSM 应用场景满足的规则:

  • 可以用状态来描述事物,并且任一时刻,事物总是处于一种状态
  • 事物拥有的状态总数是有限的
  • 通过触发事物的某些行为,可以导致事物从一种状态迁移到另一种状态
  • 事物状态变化是有规则的。A状态 -> B状态,B状态 -> C状态 ,C状态 -> A状态。
  • 同一种行为,可以将事物从多种状态变为同种状态,但是不同从同种状态变成多种状态。

落地的应用场景:

  • 网络通信协议
  • 订单,服务单,退款场景

优势

  • 代码抽象: 将业务流程进行抽象和结构化,将复杂的状态转移图,分割成相邻状态的最小单元,这样相当于搭建了乐高积木,在这套机制上可以组合成复杂的状态转移图,同时隐藏了系统的复杂度。
  • 简化流程: 业务rd只需要关注当前操作的业务逻辑(状态流转过程中的业务回调函数),极大的解耦了状态和业务。
  • 易扩展: 在新增状态或事件时,无需修改原有的状态流转逻辑,直接建立新的状态转移链路即可。
  • 业务建模: 通过最小粒度的相邻状态拼接,最终组成了业务整体的graph。

开源产品

  • cola-component-statemachine (java)

    • 优点
      • 支持condition (dsl需要再扩展)
      • 事件类型: 内部事件,外部事件
      • interceptor: 进入,退出状态机;进入,退出状态;
    • 缺点
      • 无分布式状态控制
      • 无时间触发
  • squirrel-foundation (java)

    • 优点
      • 支持动作的exit,transition,entry
      • 状态转换过程细分,可以做功能扩展和状态跟踪
      • 没有并发死锁问题
      • 轻量级
    • 缺点
      • 注解方式定义状态和事件,不支持状态和事件枚举
      • interceptor粒度粗
  • Spring statemachine (java)

    • 优点
      • Interceptor ,listener 方便监控,持久化,功能扩展
      • 对象化的状态机配置
      • 分层状态机,解决复杂场景的多状态问题
      • 使用triggers,transitions,guards,actions概念
      • 基于zk的分布式事件监听
      • 状态机配置持久化
      • 时间触发和事件触发
      • 事件类型: 内部事件,外部事件 (内部,外部是相对于状态来说的)
      • 支持spel表达式
    • 缺点
      • 重量级
      • 配合spring使用更方便
      • 单实例的StateMachine存在线程安全问题
  • Looplab fsm (go)

    • 优点
      • 支持Callback,BeforeEvent,LeaveEvent,EnterSatte,AfterEvent
      • 异常感知
      • 对状态的Mutex锁
    • 缺点
      • 无异步类型的event
      • 无分布式状态控制
      • 无condition

造个轮子

改造点

我们本节将基于Looplab fsm (go) 进行改造,改造点主要有以下几个:

  1. 同一个event下,一个现态 , 可流转到不同的次态

传统概念的状态机中,一个src和一个event的组合,只能确定一个且仅有一个的dst,但是经过改造后,一个src和一个event的组合,可能会关联多个dst,这样做并不是改变了状态机的模型,而是通过将相似的event合并,配合条件表达式,也就是组成src,event , 和条件表达式的三元组,唯一的确定可流转的dst。这样做的好处有两点:

  • 简化状态流转的配置
  • 可以将event设计的更贴合业务语义

以下单场景为例:
在这里插入图片描述
订单处于 “下单” 状态,当接收到 “创建订单” 事件时。根据订单类型的不同可以分为0元单和非0元单,传统的FSM会将两种类型的订单创建定义为两个不同的event : “创建0元订单” 和 “创建非0元订单” ,但是在bfsm中,可以只定义一个 “创建订单” 的 event ,配合条件表达式判断订单类型,将状态流转到不同的dst 。这样可以简化配置,同时也不需要将 “创建订单” 这个event做更细粒度的拆解。


  1. 匹配表达式

根据src 和 event ,能够匹配到一组 dst ,通过匹配表达式执行复杂匹配逻辑,每个匹配条件被满足后对应一个dst,在状态流转的过程中,会按照表达式的注册顺序依次进行匹配,最终会匹配执行结果为true的表达式所对应的dst ;如果所有匹配表达式执行结果都为false,那么状态不会发生流转。


  1. 可合并多场景的状态转移配置

可以将多个场景的状态转移配置合并,不合并也可以正常使用。


  1. 加锁状态流转

为应对高并发场景,支持基于redis分布式锁的状态转移,对状态转移,通过锁定状态转移的实体对象(通常为订单id,服务单id等),锁定事件fire过程,保证高并发场景下,同一实体对象的状态流程串行执行。另外,支持用户自定义锁的实现。


  1. 多对多状态配置

简化配置,提供多状态到多状态的流转配置。


  1. 状态配置的图化

基于状态流转配置,在线展示状态转移图。


Looplab fsm

示例演示

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 文件中,具体实现大家可以去阅读该源文件进行学习。


改造过程

改造的具体代码实现此处就不贴出来了,只给出流程图级别的改造说明:

在这里插入图片描述
加锁:

  • 上图省去了加锁保护细节,此处的加锁需要替换为redis分布式锁了,当然加锁这块还是可以好好优化一下的,不然高并发场景下,锁的争抢会成为瓶颈。

异常处理:

  • 状态机内部的错误会通过error的形式抛给业务方
  • 业务方的calllback函数执行异常时,需要业务方通过cancel方法主动通知状态机结束此次状态流转,但是不能再状态变更后的AfterTransCallback中调用cancel回调,因为此时状态已经发生了变更。

表达式:

  • 基于govaluate实现

多场景状态转移配置合并:

  • 可以通过场景隔离,同时抽取状态转移配置全局化,实现多场景状态转移配置合并
    在这里插入图片描述

每种场景下的配置伪代码如下:

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("场景名","初态")

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/314103.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android 13 辅助屏导航栏不显示问题

问题 在Android 13 上开启辅助屏幕。但是发现辅助屏systemui 导航按 icon没有显示,但是点击对应的区域有作用 分析 可以用 anroid device monitor 工具分析视图 解决 public NavigationBarView(Context context, AttributeSet attrs) {super(context, attrs);//add star…

x-cmd pkg | smartctl - 用于监测和分析硬盘的工具

目录 简介首次用户功能特点竞品和相关作品进一步阅读 简介 smartctl 是一个用于监测和分析硬盘中 S.M.A.R.T.(自我检测,分析和报告技术)信息的命令行工具,是 Smartmontools 的一部分。通过 smartctl 工具,可以分析各种…

安泰电子前置微小信号放大器怎么用的

前置微小信号放大器是一种重要的电子设备,用于放大微弱的输入信号,提高系统的灵敏度。它在各种领域中都有广泛的应用,包括音频、通信、测量等。在这篇文章中,我们将详细介绍前置微小信号放大器的使用方法,以便更好地理…

Cdd诊断数据控中的zz rc yy

如上图所示的Cdd Candela Diagnostic Descriptions 诊断数据库会话定义中有许多的标识符缩写,如zz rc LL xx 等 其实这些字母没有意义,它们只是唯一地标识对话框中的组合组件。

HDFS概述

文章目录 HDFS背景定义HDFS 优缺点HDFS 组成HDFS文件块大小 HDFS背景定义 背景 先给大家介绍一下什么叫HDFS,我们生活在信息爆炸的时代,随着数据量越来越大,在一个操作系统存不下所有的数据,那么就分配到更多的操作系统管理的磁…

国内镜像源配置方法(包括临时和永久方法)

国内镜像源: 阿里云 http://mirrors.aliyun.com/pypi/simple/中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/豆瓣 http://pypi.douban.com/simplePython官方 https://pypi.python.org/simple/v2ex http://pypi.v2ex.com/simple/中国科学院 http://pypi.mi…

【IDEA--dubug相关】-- 1. 取消debug的所有断点 2. debug侧边栏消失问题

下面是一些经常在日常debug时用到的场景,方便查看、与君共勉! 文章目录 1. 如何取消所有断点2. debug底部左边侧边栏消失 1. 如何取消所有断点 如图我们可能在项目中给很多代码行添加了断点,一个一个点取消麻烦 在debug运行中点击选中底部…

React Native 桥接组件封装原生组件属性

自定义属性可以让组件具备更多的灵活性,所以有必要在JS 层通过自定义属性动态传值。 一、添加原生组件属性 因为 ViewManager 管理了整个组件的行为,所以要新增组件属性也需要在这里面(如 InfoViewManager)进行定义。 1、在Inf…

[JVM] Java类的加载过程

Java类的加载过程 在Java中,类的加载是指在程序运行时将类的二进制数据加载到内存中,并转化为可以被JVM执行的形式的过程。类的加载过程主要包括以下几个步骤: 加载(Loading):通过类的全限定名,…

【Docker】数据管理之数据卷的挂载

一、什么是数据卷 为了很好的实现数据保存和数据共享,Docker提出了Volume这个概念,简单的说就是绕过默认的联合 文件系统,而以正常的文件或者目录的形式存在于宿主机上。又被称作数据卷。数据卷提供了一些有用的特性: 数据卷可以在…

Casper Network (CSPR)2024 年愿景:通过投资促进增长

Casper Network (CSPR)是行业领先的 Layer-1 区块链网络之一,通过推出了一系列值得关注的技术改进和倡议,已经为 2024 年做好了准备。 在过去的一年里,Casper Network (CSPR)不断取得里程碑式的进展,例如推…

设计模式—行为型模式之状态模式

设计模式—行为型模式之状态模式 状态(State)模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。 状态模式包含以下主要角色: 环境类&am…

构建基于RHEL9系列(CentOS9,AlmaLinux9,RockyLinux9等)的Nginx1.24.0的RPM包

本文适用:rhel9系列,或同类系统(CentOS9,AlmaLinux9,RockyLinux9等) 文档形成时期:2023年 因系统版本不同,构建部署应略有差异,但本文未做细分,对稍有经验者应不存在明显障碍。 因软件世界之复杂和个人能力…

使用主题模型和古老的人类推理进行无监督文本分类

一、说明 我在日常工作中不断遇到的一项挑战是在无法访问黄金标准标签的情况下标记文本数据。这绝不是一项微不足道的任务,在本文中,我将向您展示一种相对准确地完成此任务的方法,同时保持管道的可解释性和易于调整。 一些读者可能已经开始考…

x-cmd pkg | trash-cli - 类 Unix 系统的命令行垃圾桶

目录 简介首次用户技术特点竞品和相关作品进一步阅读 简介 trash-cli 是类 Unix 系统的命令行垃圾桶,用于移动文件到回收站,同时会记录文件的原地址和删除日期。 该工具使用与 GNOME、KDE ​​和 XFCE 等桌面环境相同的垃圾桶,所以即使是非 …

使用Web自动化测试工具显著好处

随着互联网技术的飞速发展,Web应用程序在企业中的重要性不断上升。为了确保Web应用程序的质量和稳定性,许多企业转向了Web自动化测试工具。下面是使用Web自动化测试工具的一些显著好处: 1. 提高测试覆盖率 Web自动化测试工具可以模拟用户与We…

网络中的网络 NiN

目录 1.NiN 2.代码 1.NiN 卷积层的参数等于输入的通道数*输出的通道数乘以窗口的平方,然而全连接层的参数的大小等于输入的通道乘以图片的大小乘以输出的通道数。全连接层的参数很多,占用很多的内存,占用很多的计算带宽,很容易出…

多维时序 | Matlab实现GRO-CNN-LSTM-Attention淘金算法优化卷积神经网络-长短期记忆网络结合注意力机制多变量时间序列预测

多维时序 | Matlab实现GRO-CNN-LSTM-Attention淘金算法优化卷积神经网络-长短期记忆网络结合注意力机制多变量时间序列预测 目录 多维时序 | Matlab实现GRO-CNN-LSTM-Attention淘金算法优化卷积神经网络-长短期记忆网络结合注意力机制多变量时间序列预测效果一览基本介绍程序设…

养猫家庭怎么挑选宠物空气净化器?猫用空气净化器推荐来了!

宠物空气净化器在近年来越来越受到关注,它们被宣传为解决宠物家庭空气质量问题的神器。然而,一些人认为宠物空气净化器只是商家们利用人们对宠物的爱而推出的一种所谓的“智商税”,那么作为一位养猫多年的铲屎官,我可以说宠物空气…

【c++】入门4

内联函数声明和定义不能分开 inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址 了,链接就会找不到。 auto关键字 随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在&…
最新文章