Spring 状态机
1、什么是状态机
1.1 什么是状态
先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个自动门,就有 open 和 closed 两种状态。通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如自动门的状态就是两个 open 和 closed 。
状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。例如,根据自动门的运行规则,可以抽象出下面这么一个图。
自动门有两个状态,open 和 closed ,closed 状态下,如果读取开门信号,那么状态就会切换为 open 。open 状态下如果读取关门信号,状态就会切换为 closed 。
状态机的全称是有限状态自动机,自动两个字也是包含重要含义的。给定一个状态机,同时给定它的当前状态以及输入,那么输出状态时可以明确的运算出来的。例如对于自动门,给定初始状态 closed ,给定输入“开门”,那么下一个状态时可以运算出来的。
这样状态机的基本定义就介绍完毕了。重复一下:状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
1.2 四大概念
下面来给出状态机的四大概念。
- 第一个是 State ,状态。一个状态机至少要包含两个状态。例如上面自动门的例子,有 open 和 closed 两个状态。
- 第二个是 Event ,事件。事件就是执行某个操作的触发条件或者口令。对于自动门,“按下开门按钮”就是一个事件。
- 第三个是 Action ,动作。事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。编程的时候,一个 Action一般就对应一个函数。
- 第四个是 Transition ,变换。也就是从一个状态变化为另一个状态。例如“开门过程”就是一个变换。
1.3 状态机
有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成。
其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。
2、状态机图
做需求时,需要了解以下六种元素:起始、终止、现态、次态(目标状态)、动作、条件,就可以完成一个状态机图了:
以订单为例:以从待支付状态转换为待发货状态为例
- ①现态:是指当前所处的状态。待支付
- ②条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。支付事件
- ③动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。状态转换为待发货
- ④次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。待发货 注意事项
1、避免把某个“程序动作”当作是一种“状态”来处理。那么如何区分“动作”和“状态”?“动作”是不稳定的,即使没有条件的触发,“动作”一旦执行完毕就结束了;而“状态”是相对稳定的,如果没有外部条件的触发,一个状态会一直持续下去。
2、状态划分时漏掉一些状态,导致跳转逻辑不完整。所以在设计状态机时,需要反复的查看设计的状态图或者状态表,最终达到一种牢不可破的设计方案。
3、spring statemachine
3.1 状态机spring statemachine 概述
Spring Statemachine是应用程序开发人员在Spring应用程序中使用状态机概念的框架
Spring Statemachine旨在提供以下功能:
- 易于使用的扁平单级状态机,用于简单的使用案例。
- 分层状态机结构,以简化复杂的状态配置。
- 状态机区域提供更复杂的状态配置。
- 使用触发器,转换,警卫和操作。
- 键入安全配置适配器。
- 生成器模式,用于在Spring Application上下文之外使用的简单实例化通常用例的食谱
- 基于Zookeeper的分布式状态机
- 状态机事件监听器。
- UML Eclipse Papyrus建模。
- 将计算机配置存储在永久存储中。
- Spring IOC集成将bean与状态机关联起来。
状态机功能强大,因为行为始终保证一致,使调试相对容易。这是因为操作规则是在机器启动时写成的。这个想法是应用程序可能存在于有限数量的状态中,某些预定义的触发器可以将应用程序从一个状态转移到另一个状态。此类触发器可以基于事件或计时器。
在应用程序之外定义高级逻辑然后依靠状态机来管理状态要容易得多。可以通过发送事件,侦听更改或仅请求当前状态来与状态机进行交互。
3.2 快速开始
以订单状态扭转的例子为例:
表结构设计如下:
1 | CREATE TABLE `tb_order` ( |
1)引入依赖
1 | <!-- redis持久化状态机 --> |
2)定义状态机状态和事件
状态枚举:
1 | public enum OrderStatus { |
事件:
1 | public enum OrderStatusChangeEvent { |
3)定义状态机规则和配置状态机
1 |
|
配置持久化:
1 |
|
4)业务系统
controller:
1 |
|
servie:
1 |
|
监听状态的变化:
1 |
|
3.3 测试验证
1)验证业务
- 新增一个订单http://localhost:8084/order/create
- 对订单进行支付http://localhost:8084/order/pay?id=2
- 对订单进行发货http://localhost:8084/order/deliver?id=2
- 对订单进行确认收货http://localhost:8084/order/receive?id=2
正常流程结束。如果对一个订单进行支付了,再次进行支付,则会报错:http://localhost:8084/order/pay?id=2
2)验证持久化
内存
使用内存持久化类持久化:
1 |
|
Redis持久化
引入依赖:
1 | <!-- redis持久化状态机 --> |
配置yaml:
1 | spring: |
使用redis持久化类持久化:
1 |
|
3.4 状态机存在的问题
1)stateMachine
无法抛出异常,异常会被状态机给消化掉
问题现象
从orderStateMachine.sendEvent(message);
获取的结果无法感知到。无论执行正常还是抛出异常,都返回true。
1 |
|
监听事件抛出异常,在发送事件中无法感知:
1 | private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) { |
调试发现:发送事件和监听事件是一个线程,发送事件的结果是在监听操作执行完之后才返回
监听线程:
解决方案:自己保存异常到数据库或者内存中,进行判断
也可以通过接口:org.springframework.statemachine.StateMachine##getExtendedState
方法把执行状态放入这个变量中
1 | public interface ExtendedState { |
改造监听状态:把业务的执行结果进行保存,1成功,0失败
1 |
|
发送事件改造:如果获取到业务执行异常,则返回失败,不进行状态机持久化 com.zengqingfa.springboot.state.demo.service.impl.OrderServiceImpl##sendEvent
1 |
|
代码优化
- 发送事件只针对了支付,如果是非支付事件呢?
1 | //获取到监听的结果信息 |
- 监听设置状态的代码有重复代码,需要进行优化,可使用aop
1 | try { |
常量类:
1 | public interface CommonConstants { |
支付发送事件:com.zengqingfa.springboot.state.demo.service.impl.OrderServiceImpl##pay
1 |
|
使用aop对监听事件切面,把业务执行结果封装到状态机的变量中,注解:
1 |
|
切面:
1 |
|
监听类使用注解:
1 |
|
来自: Spring 状态机