附录
附录 B:状态机概念
本附录提供有关 state machines 的一般信息。
快速示例
假设我们有名为 and 的状态和名为 and 的事件,您可以定义状态机的逻辑,如下图所示:STATE1
STATE2
EVENT1
EVENT2
以下清单定义了上图中的状态机:
public enum States {
STATE1, STATE2
}
public enum Events {
EVENT1, EVENT2
}
@Configuration
@EnableStateMachine
public class Config1 extends EnumStateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
states
.withStates()
.initial(States.STATE1)
.states(EnumSet.allOf(States.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.STATE1).target(States.STATE2)
.event(Events.EVENT1)
.and()
.withExternal()
.source(States.STATE2).target(States.STATE1)
.event(Events.EVENT2);
}
}
@WithStateMachine
public class MyBean {
@OnTransition(target = "STATE1")
void toState1() {
}
@OnTransition(target = "STATE2")
void toState2() {
}
}
public class MyApp {
@Autowired
StateMachine<States, Events> stateMachine;
void doSignals() {
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.EVENT1).build()))
.subscribe();
stateMachine
.sendEvent(Mono.just(MessageBuilder
.withPayload(Events.EVENT2).build()))
.subscribe();
}
}
词汇表
- 状态机
-
驱动状态集合的主要实体,以及区域 transitions 和 events 的 Transitions 和 Events。
- 州
-
状态 (state) 模拟了某些不变条件 holds。状态是状态机的主要实体,状态会发生变化 由事件驱动。
- 扩展状态
-
扩展状态是保存在状态中的一组特殊变量 machine 来减少所需状态的数量。
- 过渡
-
过渡是源状态和目标之间的关系 州。它可能是复合转换的一部分,该转换采用状态 machine 从一个 state 配置切换到另一个 state 配置,表示完整的 状态机对 特定类型。
- 事件
-
一个实体,它被发送到状态机,然后驱动各种 状态更改。
- 初始状态
-
状态机启动的特殊状态。初始状态为 始终绑定到特定的状态机或区域。状态 具有多个区域的计算机可能具有多个初始状态。
- 结束状态
-
(也称为最终状态。一种特殊的状态表示 表示封闭区域已完成。如果封闭区域为 直接包含在状态机和 state machine 也都已完成,则整个 state 机器完成。
- 历史记录状态
-
一个伪状态,让状态机记住它的最后一个 active 状态。存在两种类型的历史状态:浅层(即 仅记住顶级状态)和 deep(记住 子机器)。
- 选择状态
-
一个伪状态,允许根据 (例如) 事件标头或扩展状态变量。
- 交汇点状态
-
一个伪状态,它与 choice state 相对相似,但允许 多个传入过渡,而 choice 只允许一个传入 过渡。
- 分叉状态
-
一种伪状态,用于受控地进入区域。
- 加入状态
-
一种伪状态,它提供对区域的受控退出。
- 入场点
-
允许受控进入 submachine的伪状态。
- 退出点
-
允许从子机中受控退出的伪状态。
- 地区
-
区域是复合状态或状态的正交部分 机器。它包含状态和转换。
- 警卫
-
根据 扩展状态变量和事件参数。守卫条件影响 通过启用 actions 或 transitions 实现状态机的行为 仅当他们评估时禁用他们 自。
TRUE
FALSE
- 行动
-
操作是在触发 过渡。
状态机速成课程
本附录提供了状态机的通用速成课程 概念。
状态
状态是状态机所在的模型。它总是 更容易将 state 描述为真实世界的示例,而不是尝试使用 抽象概念在通用文档中。为此,请考虑 一个简单的键盘例子 — 我们大多数人每天都会使用一个。 如果您有一个完整的键盘,左侧有普通键,并且 数字键盘,您可能已经注意到了 数字键盘可能处于两种不同的状态,具体取决于 numlock 已激活。如果它未激活,请按数字键盘键 使用箭头等进行导航。如果数字键盘处于活动状态,请按 这些键会导致键入数字。本质上,键盘的数字键盘部分 可以处于两种不同的状态。
将状态概念与编程相关联,这意味着不是使用 标志、嵌套的 if/else/break 子句或其他不切实际(有时是曲折的)逻辑,您可以 依赖状态、状态变量或与 状态机。
伪状态
伪状态是一种特殊类型的状态,通常会引入更多 更高级别的 logic 添加到状态机中,方法是给状态一个 特殊含义(如 initial state)。然后,状态机可以在内部 通过执行 UML 状态中可用的各种操作来对这些状态做出反应 机器概念。
初
每个状态始终需要 Initial pseudostate state state machine 的 Machine,无论您拥有简单的单级状态机还是更多的 由子机或区域组成的复杂状态机。初始 state 定义状态机启动时应去哪里。 没有它,状态机就会出现错误。
结束
Terminate 伪状态(也称为 “end state”) 表示 特定状态机已达到其最终状态。有效 这意味着状态机不再处理任何事件,并且 不转机到任何其他州。但是,如果 submachines 是 regions,则状态机可以从其 terminal 状态重新启动。
选择
您可以使用 Choice 伪状态选择 从此状态过渡。动态条件由守卫评估 ,以便选择一个分支。通常为 使用简单的 if/elseif/else 结构来确保 branch 处于选中状态。否则,状态机可能会陷入死锁。 并且配置格式不正确。
结
Junction 伪状态在功能上类似于 choice,因为两者都是 使用 if/elseif/else 结构实现。唯一真正的区别是 该 Junction 允许多个传入转换,而 CHOICE 只允许一个。因此,差异在很大程度上是学术性的,但确实有一些 差异(例如,何时设计状态机)与实际 UI 建模一起使用 框架。
历史
您可以使用 History 伪状态来记住最后一个活动状态
配置。退出状态机后,您可以使用历史状态
以恢复以前已知的配置。有两种类型
的可用历史记录状态:(它只记住
状态机本身)和 (它也记住嵌套状态)。SHALLOW
DEEP
历史状态可以通过侦听状态在外部实现 machine 事件,但这很快就会变得非常困难的 logic, 特别是当状态机包含复杂的嵌套结构时。 让状态机本身处理历史状态的记录 让事情变得简单得多。用户只需创建一个 transition 转换为 history 状态,状态机会处理所需的 logic 返回到其上一个已知的记录状态。
如果 Transition 在历史状态上终止,当状态 之前未输入(换句话说,不存在以前的历史记录)或已达到其 end 状态,则 transition 可以强制状态机进入特定的子状态,方法是 使用默认的 History 机制。此过渡源于 并终止于特定顶点(默认 history state) 的 STATE 的 STATE 中。此过渡是 仅当其执行导致历史状态并且该状态以前从未过时时才采取 积极。否则,将执行该区域的正常历史记录条目。 如果未定义默认历史记录过渡,则 执行 Region。
叉
您可以使用 Fork 伪状态对一个或多个区域进行显式输入。 下图显示了 fork 的工作原理:
目标状态可以是托管区域的父状态,它只是 表示通过输入区域的初始状态来激活区域。你 还可以将目标直接添加到区域中的任何状态,这 允许更受控地进入状态。
守卫条件
保护条件是基于扩展状态变量和事件参数计算结果为 或 的表达式。警卫
与 action 和 transitions 一起使用,以动态选择
应运行特定的操作或过渡。警卫的各方面,
事件参数和扩展状态变量的存在来生成状态
机器设计要简单得多。TRUE
FALSE
事件
Event 是驱动状态机最常用的触发器行为。 还有其他方法可以在状态机中触发行为 (例如计时器),但事件才是真正允许用户 与状态机交互。事件也称为 “信号”。 它们基本上表示可能改变状态机状态的东西。
转换
过渡是源状态和目标之间的关系 州。从一种状态切换到另一种状态是导致的状态转换 通过触发器。
行动
Action 确实会粘附状态机状态更改 添加到用户自己的代码中。状态机可以在各种 更改和状态机中的步骤(例如进入或退出状态) 或执行状态转换。
操作通常可以访问状态上下文,该上下文提供运行 编写 CHOICE 代码以各种方式与状态机交互。 状态上下文公开了整个状态机,以便用户可以 访问扩展状态变量、事件标头(如果转换基于 在事件上)或实际过渡(可以看到更多 详细说明此 state 更改的来源和去向)。
分层状态机
分层状态机的概念用于简化状态 当特定状态必须共存时进行设计。
分层状态实际上是 UML 状态机的一项创新
传统的状态机,如 Mealy 或 Moore 机。
Hierarchical states 允许您定义某种级别的抽象(parallel
到 Java 开发人员如何使用 abstract 定义类结构
类)。例如,对于嵌套状态机,您可以
在多个状态级别上定义过渡(可能使用
不同的条件)。状态机总是尝试查看当前
state 能够处理事件,并且 transition guard
条件。如果这些条件的计算结果未为 ,则状态
机器只能看到 Super State 可以处理什么。TRUE
地区
通常查看区域(也称为正交区域) 作为应用于状态的异 OR (XOR) 运算。区域中的概念 状态机的术语通常有点难以理解, 但是通过一个简单的例子,事情会变得简单一些。
我们中的一些人有一个全尺寸键盘,主键在左侧,数字 键。您可能已经注意到,双方真的 都有自己的状态,如果您按下 “numlock” 键(它 仅改变数字键盘本身的行为)。如果您没有全尺寸 键盘,您可以购买外部 USB 数字键盘。 假设键盘的左侧和右侧都可以存在,而没有 其他,它们必须具有完全不同的状态,这意味着它们是 在不同的状态机上操作。在状态机术语中,一个 键盘是一个区域,数字键盘是另一个区域。
处理两个不同的 状态机作为完全独立的实体,因为它们 仍然以某种方式一起工作。这种独立性允许正交区域 在单个状态中以多个同步状态组合在一起 在状态机中。
附录 C:分布式状态机技术论文
本附录提供了有关 将 Zookeeper 实例与 Spring Statemachine 一起使用。
抽象
在单个状态机之上引入“分布式状态”
在单个 JVM 上运行的实例是一个困难且复杂的主题。
“分布式状态机”的概念引入了一些相对复杂的
由于运行到完成
模型,更一般地说,由于其单线程执行模型
尽管正交区域可以并行运行。另一种天然
问题是状态机转换执行是由触发器驱动的,
它们要么是基于 or 的。event
timer
Spring State Machine 尝试解决 spanning 问题 通过支持分布式 状态机。在这里,我们向您展示了您可以使用 generic 跨多个 JVM 和 Spring 的“状态机”概念 应用程序上下文。
我们发现,如果仔细选择抽象
支持分布式状态存储库可以保证准备就绪,这是
可以创建一个可以共享的
distributed 状态。Distributed State Machine
CP
我们的结果表明,如果支持 repository 是 “CP” (稍后讨论)。 我们预计我们的分布式状态机可以提供 需要使用共享分布式的应用程序的基础 国家。该模型旨在为云应用程序提供良好的方法 拥有更简单的相互通信方式,而无需 显式构建这些分布式状态概念。
介绍
Spring State Machine 不强制使用单线程执行 model 执行,因为一旦使用了多个 Region,Region 就可以在 parallel (如果应用了必要的配置)。这是一个重要的 topic 的 Topic,因为一旦用户想要拥有并行状态机 执行,它使独立区域的状态更改更快。
当状态更改不再由本地 JVM 或 本地状态机实例,需要控制转换逻辑 外部的任意持久存储中。此存储需要 有办法在分发时通知参与的状态机 state 已更改。
CAP 定理指出 分布式计算机系统不可能同时 提供以下所有三个保证:一致性、 可用性和分区容错性。
这意味着, 无论选择什么作为后备持久化存储,都是可取的 改为 “CP”。在这种情况下,“CP”的意思是“一致性”和“分区” 宽容”。自然,分布式 Spring Statemachine 并不关心 关于其 “CAP” 级别,但实际上为 “一致性” 和 “分区容错性”比“可用性”更重要。这是 (例如)Zookeeper 使用“CP”存储的确切原因。
本文中介绍的所有测试都是通过运行自定义 Jepsen 在以下环境中进行测试:
-
具有节点 n1、n2、n3、n4 和 n5 的集群。
-
每个节点都有一个实例,该实例构造一个 ensemble 所有其他节点。
Zookeeper
-
每个节点都安装了一个 Web 示例, 以连接到本地节点。
Zookeeper
-
每个状态机实例仅与本地实例通信。将计算机连接到多个实例时 是可能的,这里不使用。
Zookeeper
-
所有状态机实例在启动时,都会使用 Zookeeper ensemble 创建一个。
StateMachineEnsemble
-
每个示例都包含一个自定义的 rest API,Jepsen 使用该 API 发送 事件并检查特定状态机状态。
Jepsen 的所有测试均可从 Jepsen 获得
测试。Spring Distributed Statemachine
通用概念
a 的一个设计决策是不让每个
单个状态机实例,请注意它是
“分布式集成”。由于 a 的主要功能和特性可以通过其界面访问,因此
将此实例包装在 中,该实例
拦截所有状态机通信并与
ensemble 来编排分布式状态更改。Distributed State Machine
StateMachine
DistributedStateMachine
另一个重要的概念是能够足够持久
来自状态机的信息,用于重置状态机状态
从任意状态转换为新的反序列化状态。这很自然
当新的状态机实例加入 ensemble 时需要
并且需要将自己的内部 state 与分布式
州。连同使用分布式 state 和 state 的概念
persist 时,可以创建分布式状态机。
目前,唯一的后备仓库是
使用 Zookeeper 实现。Distributed State Machine
如使用分布式状态中所述,分布式状态由
将 a 的实例包装在 .具体实现方式为
与 Zookeeper 集成。StateMachine
DistributedStateMachine
StateMachineEnsemble
ZookeeperStateMachineEnsemble
的作用ZookeeperStateMachinePersist
我们想要一个通用接口 ()
可以持久保存到任意存储中,并为 实现此接口。StateMachinePersist
StateMachineContext
ZookeeperStateMachinePersist
Zookeeper
的作用ZookeeperStateMachineEnsemble
虽然分布式状态机使用一组序列化的
contexts 来更新自己的 state,使用 zookeeper,我们有一个
关于如何监听这些上下文变化的概念问题。我们
可以将上下文序列化为 zookeeper,并最终
在数据被修改时监听。但是,没有
保证您每次数据更改都会收到通知,
因为 A 的注册一旦触发就会被禁用
并且用户需要重新注册该 .在这短短的时间里,
可以更改数据,从而导致丢失事件。是的
实际上很容易通过更改数据来错过这些事件
多个线程。znode
znode
Zookeeper
watcher
znode
watcher
znode
为了解决这个问题,我们保留了单独的上下文更改
in multiple 中,我们使用一个简单的整数计数器来标记
这是当前活动的。这样做可以让我们重播错过的
事件。我们不想创建越来越多的 znodes,然后再创建
删除旧的。相反,我们使用循环的简单概念
znode 集。这允许我们使用一组预定义的 znode,其中
当前节点可以用一个简单的整数计数器来确定。我们已经有
此计数器通过跟踪主数据版本 (在 中是一个整数) 。znodes
znode
znode
Zookeeper
循环缓冲区的大小必须为 2 的幂,以避免 当整数溢出时出现问题。因此,我们不需要 处理任何特定情况。
分布式容差
显示针对状态的各种分布式操作 机器在现实生活中工作时,我们使用一组 Jepsen 测试 模拟在实际分布式 簇。这些包括网络级别的“大脑分裂”、并行 具有多个“分布式状态机”的事件,以及 “扩展状态变量”。Jepsen 测试基于一个示例 Web,该示例实例在 多个主机以及每个节点上的 Zookeeper 实例 运行状态机的位置。本质上,每个状态机样本 连接到本地 Zookeeper 实例,这允许我们使用 Jepsen,用于模拟网络条件。
本章后面显示的绘图包含的状态和事件 直接映射到状态图,您可以在 Web 中找到该图。
孤立的事件
将隔离的单个事件发送到 ensemble 是最简单的测试场景,它演示了 一个状态机中的状态更改会正确地传播到其他状态机 集成中的状态机。
在此测试中,我们演示了一台计算机中的状态更改 最终导致其他计算机发生一致的状态更改。 下图显示了测试状态机的事件和状态更改:
在上图中:
-
所有计算机都报告 状态 。
S21
-
事件被发送到节点,并且所有节点都报告状态更改 从 到 。
I
n1
S21
S22
-
事件被发送到节点,并且所有节点都报告状态更改 从 到 。
C
n2
S22
S211
-
事件被发送到节点,并且所有节点都报告状态更改 从 到 。
I
n5
S211
S212
-
事件被发送到节点,并且所有节点都报告状态更改 从 到 。
K
n3
S212
S21
-
我们循环事件 、 、 ,再一次,通过随机节点。
I
C
I
K
平行活动
多个分布式状态机的一个逻辑问题是,如果 同一事件以完全相同的方式发送到多个状态机 time 时,这些事件中只有一个会导致分布式状态 转换。这在某种程度上是意料之中的场景,因为第一个状态 machine(对于此事件)能够更改分布式状态 控制分布式转换逻辑。实际上,所有其他 接收到此相同事件的计算机会以静默方式丢弃该事件, 因为分布式状态不再处于特定的 事件。
在下图所示的测试中,我们演示了由 parallel 事件最终会导致 所有计算机中的一致状态更改:
在上图中,我们使用了与上一个示例相同的事件流 (隔离事件),不同之处在于事件始终是 发送到所有节点。
并发扩展状态变量更改
扩展状态机变量不保证在 任何给定时间,但是,在分布式状态更改后,所有状态机 在 ensemble 中应该有一个 synchronized extended 状态。
在此测试中,我们演示了扩展状态的更改 一个分布式状态机中的变量最终会变成 在所有分布式状态机中保持一致。 下图显示了此测试:
在上图中:
-
将事件发送到事件变量值为 的节点。然后,所有节点都报告具有一个名为 的变量。
J
n5
testVariable
v1
testVariable
v1
-
Event 从 variable 到 重复执行相同的检查。
J
v2
v8
分区容错
我们需要始终假设,迟早会在集群中 变坏,无论是 Zookeeper 实例的崩溃,还是 state 机器崩溃,或网络问题,例如“大脑分裂”。(脑分裂是一个 现有集群成员被隔离,因此只有 部分主机能够看到彼此)。通常的情况是 brain split 创建 ensemble 的集合,使得少数主持人无法参与集合 直到网络状态已修复。
在下面的测试中,我们证明了各种类型的大脑分裂成 一个 ensemble 最终会导致所有 分布式状态机。
有两种情况在
网络,其中 where 和 instances 所在的
拆分为两半(假设每个都连接到
local 实例):Zookeeper
Statemachine
Statemachine
Zookeeper
-
如果当前的 zookeeper leader 保持多数,则所有 Client 端 连接到大多数保持正常运行。
-
如果当前的 zookeeper 领导者是少数,则所有客户端 断开与它的连接并尝试重新连接到上一个 少数成员已成功重新加入现有的多数成员 整体。
在我们当前的 Jepsen 测试中,我们无法分离 Zookeeper 裂脑 领先者处于多数或少数状态之间的情况,因此我们需要 多次运行测试以完成此情况。 |
在下图中,我们将状态机错误状态映射到 an,以指示状态机处于错误状态,而不是
正常状态。在解释图表状态时,请记住这一点。error |
在第一个测试中,我们表明,当现有的 Zookeeper 领导者 保持大多数,五分之三的机器继续保持原样。 下图显示了此测试:
在上图中:
-
第一个事件 被发送到所有计算机,导致状态更改为 。
C
S211
-
Jepsen 复仇女神会导致大脑分裂,从而导致分裂 的 和 。节点是少数,并且 节点构建一个新的健康多数。节点 大多数节点保持正常运行,但少数节点 进入 ERROR 状态。
n1/n2/n5
n3/n4
n3/n4
n1/n2/n5
-
Jepsen 修复网络,一段时间后,节点加入 返回到 ensemble 并同步其 distributed 状态。
n3/n4
-
最后,将 event 发送到所有状态机,以确保 ensemble 工作正常。此状态更改会导致返回状态 。
K1
S21
在第二个测试中,我们表明,当现有的 zookeeper leader 是 保持少数,所有机器都会出错。 下图显示了第二个测试:
在上图中:
-
第一个事件 将发送到所有计算机,从而导致状态更改为 。
C
S211
-
Jepsen 复仇女神会导致大脑分裂,从而导致分裂 这样,现有的领导人就保持在少数派和所有 实例与 Ensemble 断开连接。
Zookeeper
-
Jepsen 修复网络,一段时间后,所有节点都加入 返回到 ensemble 并同步其 distributed 状态。
-
最后,将 event 发送到所有状态机,以确保 ensemble 正常工作。此状态更改会导致返回状态 。
K1
S21
碰撞和合并容差
在这个测试中,我们演示了杀死一个现有的状态机 然后将新实例连接回 ensemble 中,将 分布式状态 Healthy 和新加入的状态机同步 他们的状态正确。 下图显示了崩溃和连接容差测试:
在此测试中,不会检查第一个 和最后一个 之间的状态。
因此,该图在两者之间显示一条平坦线。检查状态
正是 和 之间状态变化发生的位置。X X S21 S211 |
在上图中:
-
所有状态机都从初始状态 () 转换为 state 的 intent 中,这样我们就可以在 join 期间测试正确的 state synchronize。
S21
S211
-
X
标记特定节点何时崩溃并启动。 -
同时,我们从所有机器请求状态并绘制结果。
-
最后,我们做一个简单的转换,从 from 到 make 确保所有状态机仍然正常运行。
S21
S211
开发人员文档
本附录为开发人员提供了通用信息,他们可能会 想要贡献或其他想要了解 state 的人 机器工作或理解其内部概念。
StateMachine 配置模型
StateMachineModel
和其他相关的 SPI 类是抽象的
在各种 configuration 和 factory 类之间。这也允许
其他人可以更轻松地集成以构建状态机。
如下面的清单所示,您可以通过构建模型来实例化状态机 使用配置数据类,然后要求工厂构建 状态机:
// setup configuration data
ConfigurationData<String, String> configurationData = new ConfigurationData<>();
// setup states data
Collection<StateData<String, String>> stateData = new ArrayList<>();
stateData.add(new StateData<String, String>("S1", true));
stateData.add(new StateData<String, String>("S2"));
StatesData<String, String> statesData = new StatesData<>(stateData);
// setup transitions data
Collection<TransitionData<String, String>> transitionData = new ArrayList<>();
transitionData.add(new TransitionData<String, String>("S1", "S2", "E1"));
TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData);
// setup model
StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData,
transitionsData);
// instantiate machine via factory
ObjectStateMachineFactory<String, String> factory = new ObjectStateMachineFactory<>(stateMachineModel);
StateMachine<String, String> stateMachine = factory.getStateMachine();