附录
附录B:状态机概念
本附录提供了关于状态机的通用信息。
快速示例
假设我们有州名州1和STATE2以及事件名称事件1和事件2,你可以定义状态机的逻辑,如下图所示:
以下列表定义了前一图中的状态机:
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();
}
}
词汇表
- 状态机
-
驱动一组州和区域的主要实体, 转折和事件。
- 州
-
一个状态模拟了某一不变条件的情形 保持。状态是状态机的主要实体,状态发生变化 被事件驱动。
- 扩展状态
-
扩展状态是一组特殊的变量,保持在某个状态中 减少所需状态数量的机器。
- 过渡
-
转移是源态与目标之间的关系 州。它可能是复合跃迁的一部分,该跃迁取 机器从一种状态配置到另一种状态配置,表示完整的 状态机对事件发生的响应 特定类型。
- 事件
-
一个被发送到状态机并驱动 状态变化。
- 初始状态
-
一种特殊状态,状态机在该状态下启动。初始状态为 总是绑定在特定的状态机或区域上。一个国家 具有多个区域的机器可能具有多个初始状态。
- 终局
-
(也称为终极状态。)一种特殊的状态,表示 围合区域已完成。如果包围区域为 直接包含在状态机中,以及所有其他区域 状态机也已完成,整个状态 机器完成了。
- 历史州
-
一种让状态机记住其最后一次的伪状态 活跃状态。存在两种历史状态:浅层(即 只记住顶层状态)和深层(DEEP,记得活跃状态,在 子机器)。
- 选择州
-
一个允许基于(例如)做出转移选择的伪态 事件头或扩展状态变量。
- 交汇州
-
一种伪状态,与选择状态相对相似,但允许 多个入射转移,而选择权只允许一个入射 过渡。
- 叉州
-
一个准国家,允许对某个地区的受控进入。
- 加入州
-
一种伪状态,提供区域的受控出口。
- 入口
-
一种允许受控进入子机器的伪状态。
- 出口
-
一种允许从子机器中受控退出的伪状态。
- 地区
-
区域是复合状态或状态的正交部分 机器。它包含状态和转移。
- 警卫
-
基于 的值动态计算的布尔表达式 扩展状态变量和事件参数。守护条件影响 状态机通过启用动作或转移来表现 只有当它们评估到
true评估时禁用 自false. - 行动
-
动作是在触发 过渡。
状态机速成课程
本附录提供了状态机的通用速成课程 概念。
状态
状态是一种模型,其中状态机可以存在。总是这样 用真实世界的例子来描述状态比试图用它更容易 抽象概念 通用文档。为此,考虑一下 一个简单的键盘例子——我们大多数人每天都在用一台。 如果你有一把全键盘,左侧有正常按键, 右侧的数字键盘,你可能注意到 数字键盘可能处于两种不同状态,具体取决于 numlock被激活。如果没有激活,按数字键盘键 通过箭头等方式实现导航。如果数字键盘处于激活状态,按下 这些键会导致输入数字。本质上,就是键盘的数字键盘部分 可以处于两个不同的状态。
将状态概念与编程联系起来,意味着 旗帜、嵌套的if/else/break子句,或者其他不切实际(有时甚至复杂)的逻辑,你可以 依赖状态、状态变量或与 状态机。
伪状态
伪态是一种特殊类型的状态,通常会引入更多 通过给出状态 A 特殊含义(如初始状态)。状态机随后可以内部进行 通过执行UML状态中可用的各种动作来应对这些状态 机器概念。
选择
你可以使用Choice伪状态选择动态条件分支 这是从这种状态的转变。动态条件由守卫评估 这样一个分支就被选中了。通常是 简单的if/elseif/else结构用于确保 分支已选定。否则,状态机可能会陷入死锁, 而且该构型是不成形的。
结
结的伪态在功能上与选择相似,因为两者都是 通过IF/elseif/else结构实现。唯一真正的区别是 该交汇允许多个入射跃迁,而选择 只允许一个。因此,差异主要是学术上的,但确实存在一些 差异,比如在真实的用户界面建模中,比如状态机的设计 框架。
历史
你可以用历史伪状态来记住最后一个活跃状态
配置。状态机退出后,你可以使用历史状态
以恢复之前已知的配置。有两种类型
历史上有以下资料:浅(只记住 a 的主动状态
状态机本身)以及深(它也记住嵌套态)。
历史状态可以通过监听状态实现 但这很快会让逻辑变得非常困难, 尤其是当状态机包含复杂的嵌套结构时。 让状态机本身处理历史状态的记录 这让事情简单多了。用户只需创建一个 切换到历史状态,状态机处理所需的 逻辑上回溯到最后已知的记录状态。
当过渡终止于历史状态时 之前未被录入(换句话说,没有先前的历史记录),或者已经达到了 在结束状态时,转移可以强制状态机进入特定的子状态,通过以下方式 使用默认历史机制。这一转变起源于 在历史状态中,终止于特定顶点(默认历史) 包含历史州的地区。这个转变是 只有当执行导致历史状态且国家以前从未存在过时才被采纳 积极。否则,执行该区域的正常历史录入。 如果没有定义默认历史转移,则标准默认条目为 区域表演。
叉
你可以用 Fork 伪状态对一个或多个区域做显式的录入。 下图展示了叉的工作原理:
目标状态可以是承载区域的父态,简单地 意味着区域通过进入初始状态被激活。你 也可以直接将目标添加到区域内的任何状态,其中 允许更受控地进入状态。
警卫条件
守护条件是将值值为以下true或false,基于扩展状态变量和事件参数。警卫
与动作和转移一起使用,以动态选择是否
应执行特定的动作或过渡。守卫的各个方面,
存在事件参数和扩展状态变量来生成状态
机器设计要简单得多。
行动
动作实际上是将状态机状态变化粘合起来的 对用户自己的代码。状态机可以对 在状态机中的步骤(例如进入或退出状态)上的变化 或者做状态转换。
动作通常可以访问状态上下文,从而实现运行 编写一个选择以多种方式与状态机交互的选项。 状态上下文暴露了整个状态机,使用户能够 访问扩展状态变量、事件头部(如果基于转换) 在事件上),或实际过渡(可以看到更多内容) 详细说明了这种状态变化的来源和未来方向)。
分层状态机
层级状态机的概念被用来简化状态 当特定状态必须共存时。
分层状态实际上是UML状态机中的一项创新
传统的状态机,如Mealy机或Moore机。
层级状态允许你定义某种抽象层次(并行
到Java开发者如何用抽象定义类结构
课程)。例如,使用嵌套状态机,你可以
定义多层态上的跃迁(可能为
不同的条件)。状态机总是试图检测电流是否
州能够处理事件,同时与过渡守卫一起
条件。如果这些条件不值true,状态
机器只是看看超级国家能承受什么。
地区
通常会将区域(也称为正交区域)视为 作为应用到状态上的异或(XOR)运算。区域的概念 状态机的术语通常有点难以理解, 但用一个简单的例子,事情会变得简单一些。
我们中的一些人拥有一个全尺寸键盘,主键在左侧,数字键在右侧。你可能已经注意到,两边实际上都有各自的状态,你可以通过按下“数字锁”键(它只改变数字键盘本身的行为)来看到。如果你没有全尺寸的键盘,你可以购买一个外接的USB数字键盘。鉴于键盘的左右两侧可以各自存在,而对方,它们必须拥有完全不同的状态,这意味着它们运行在不同的状态机上。用状态机术语来说,键盘的主要部分是一个区域,数字键盘是另一个区域。
处理两个不同的状态机作为完全独立的实体会有些不便,因为它们仍然以某种方式协同工作。这种独立性使得正交区域能够在单一状态内以多个同时状态组合在一起在状态机中。
附录C:分布式状态机技术论文
本附录提供了关于使用 Zookeeper 实例与 Spring 状态机的详细技术文档。
抽象
在单一状态机之上引入“分布式状态”运行在单一JVM上的实例是一个困难且复杂的话题。“分布式状态机”的概念引入了一些相对复杂的问题,基于简单的状态机,因为它采用了运行至完成的模型,更广义地说,由于其单线程执行模型,尽管正交区域可以并行运行。另一个自然的问题是状态机的转换执行由触发器驱动,这些触发器是事件或定时器基于。
Spring状态机试图通过支持分布式状态机来解决生成问题通过支持分布式状态机。这里我们展示了你可以在多个JVM和Spring应用上下文中使用通用“状态机”概念。
我们发现,如果分布式状态机抽象是经过精心选择的并且支持分布式状态仓库CP准备状态,即可以创建一个一致的状态机,该状态机能够在集合中共享分布式状态。
我们的结果表明,如果存储库是“CP”(稍后讨论),分布式状态变化是一致的。我们预期我们的分布式状态机能够为需要与共享分布式工作的应用提供基础 国家。 该模型旨在为云应用提供良好的方法使彼此之间通信更便捷,而无需明确构建这些分布式状态概念。
介绍
Spring状态机不强制使用单线程执行模型,因为一旦多个区域被使用,区域可以在并行中执行,前提是应用必要的配置。这是一个重要的话题,因为一旦用户想要并行状态机执行,它使独立区域的状态变化更快。
当状态变化不再由本地JVM或本地状态机实例中的触发器驱动时,转换逻辑需要被控制外部控制在任意的持久存储中。该存储需要具备一种方式,在分布式状态发生变化时通知参与的状态机。状态发生变化。
CAP定理指出分布式计算机系统不可能同时提供以下三项保证:一致性,可用性和分区容忍度。
这意味着,无论选择什么作为后备持久存储,都建议使用“CP”。在此语境中,“CP”指的是“一致性”和“分区容忍度”。自然,分布式的Spring状态机并不关心其“CAP”级别,但实际上,“一致性”和“分区容差”比“可用性”更重要。 这是 这正是(例如)Zookeeper 使用“CP”存储的确切原因。
本文中介绍的所有测试均通过自定义运行完成 Jepsen在以下环境中进行测试:
-
一个节点为n1、n2、n3、n4和n5的集群。
-
每个节点都有一个
Zookeeper构造一个集合的实例,使得 所有其他节点。 -
每个节点都安装了一个网页样本, 连接本地
Zookeeper节点。 -
每个状态机实例只与本地通信
Zookeeper实例。连接机器到多个实例时 是可能的,但这里不使用。 -
所有状态机实例在启动时,都会创建一个
StateMachineEnsemble通过使用Zookeeper 服装。 -
每个样本都包含自定义的休止 API,Jepsen 利用该 API 发送 事件并检查特定状态机状态。
所有Jepsen测试Spring分布式状态机可从Jepsen获得
测试。
通用概念
一个设计决策分布式状态机不是要做每一个
个别状态机实例请注意它是
“分布式合奏”。因为状态机可以通过其接口访问,这样做很合理
用分布式状态机哪
拦截所有状态机通信并与
集合体用于协调分布式状态变化。
另一个重要概念是能够坚持得足够多
来自状态机的信息用于重置状态
从任意状态转变为新的反串行状态。这是很自然的
当新的状态机实例与集合加入时需要
并且需要与分布式
州。结合分布式状态和状态的概念
持续存在,可以创建分布式状态机。
目前,唯一的后备仓库是分布式状态机是
通过使用 Zookeeper 实现。
如《使用分布式状态》中提到的,分布式状态的实现方式是
包裹 a 实例状态机在分布式状态机.具体StateMachineEnsemble实现为ZookeeperStateMachineEnsemble提供
与Zookeeper的集成。
角色ZookeeperStateMachinePersist
我们想要一个通用的界面(StateMachinePersist)
可以持续存在状态机上下文进入任意存储和ZookeeperStateMachinePersist实现了该接口Zookeeper .
角色ZookeeperStateMachineEnsemble
而分布式状态机则使用一组序列化的
上下文以更新自身状态,使用 Zookeeper,我们有一个
关于如何聆听这些语境变化的概念性问题。我们
可以将上下文序列化为动物饲养员Znode最终
听Znode数据被修改。然而Zookeeper 不
确保每次数据变更都会收到通知,
因为我登记了观察家对于Znode一旦发射,就会被禁用
用户需要重新注册该信息观察家.在这短暂的时间里,
一个Znode数据可以被更改,因此会出现缺失的事件。是的
实际上,通过更改数据很容易错过这些事件
多个线程同时进行。
为了解决这个问题,我们保留了个别的上下文变化
多重znodes我们用一个简单的整数计数器来标记
哪Znode是当前活跃的。这样做可以让我们重播错过的比赛
事件。我们不想创建越来越多的znode,然后再以后再来
删除旧的。相反,我们使用了简单的循环概念
一组znodes。这让我们可以使用一组预定义的z节点,其中
当前节点可以用简单的整数计数器确定。我们已经有了
通过跟踪主线来计数Znode数据版本(其中,在Zookeeper ,是一个整数)。
圆形缓冲区的大小被要求为二的幂,以避免 当整数溢出时出现麻烦。因此,我们不必如此 处理具体案件。
分布容忍
为了展示各种分布式动作如何针对一个状态机器在现实生活中工作,我们使用一组Jepsen测试来模拟真实分布式中可能发生的各种条件 簇。 这些包括网络层面的“脑分”、并行多个“分布式状态机”事件,以及“扩展状态变量”的变化。Jepsen 测试基于一个示例 Web,该示例实例运行在多个主机上,每个节点上都有一个 Zookeeper 实例状态机在此运行。本质上,每个状态机样本都连接到本地的 Zookeeper 实例,这使我们能够通过使用Jepsen 来模拟网络状况。
本章后面展示的图包含状态和事件直接映射到状态图,你可以在网页中找到。
孤立事件
将孤立的单一事件发送到一个集合中恰好有一个状态机,是最简单的测试场景,它证明了一个状态机中的状态变化能够正确传播到另一个集合中的状态机。
在本测试中,我们展示了一台机器的状态变化最终会导致其他机器的状态发生一致的变化。下图展示了测试状态机的事件和状态变化:
在前图中:
-
所有机器报告状态
S21. -
事件
我发送到节点N1所有节点报告状态变化 从S21自第二季. -
事件
C发送到节点N2所有节点报告状态变化 从第二季自S211. -
事件
我发送到节点N5所有节点报告状态变化 从S211自S212. -
事件
K发送到节点N3所有节点报告状态变化 从S212自S21. -
我们循环活动
我,C,我和K再来一次,通过随机节点。
平行事件
多个分布式状态机的一个逻辑问题是,如果同一事件在多个状态机中恰好同时发送,只有其中一个事件会导致分布式状态 转换。 这在某种程度上是可以预料的情景,因为第一个状态能够改变分布式状态的机器控制分布式的转换逻辑。实际上,所有其他接收到相同事件的机器都会静默地丢弃该事件,因为分布式状态不再处于可以处理特定事件的状态。
在下图所示的测试中,我们展示了由集合中并行事件引起的状态变化最终导致所有机器都出现一致的状态变化:
在前一张图片中,我们使用了与前一个样本相同的事件流程(孤立事件),区别在于事件总是发送到所有节点。
并发扩展状态变量变更
扩展状态机变量并不保证在任一时间点都是原子的,但在分布式状态变化后,所有状态机在集合中都应具有同步的扩展状态。
在本测试中,我们展示了扩展状态的变化一个分布式状态机中的变量最终会在所有分布式状态机中保持一致。下图展示了该测试:
在前图中:
-
事件
J是发送到节点N5事件变量为测试变量具有价值第一版. 所有节点随后报告都有一个变量命名测试变量其值为第一版. -
事件
J是从变量中重复出现的V2自V8,做同样的检查。
分区容差
我们必须始终假设,迟早,集群中的某些东西会出问题,无论是Zookeeper实例崩溃、状态机器崩溃,还是网络问题,如“大脑分裂”。(大脑分裂是指现有集群成员被隔离,只有主机的部分能够相互看到)。通常的情形是大脑分裂会创建少数和多数的分区集成,使得少数主机无法参与集合直到网络状态恢复。
在接下来的测试中,我们展示了各种类型的大脑分裂最终导致所有分布式状态机达到完全同步的状态。
有两种情况是大脑分裂成
网络,其中Zookeeper 和状态机实例如下
一分为二(假设各自)状态机连接到一个
当地Zookeeper 实例):
-
如果现任动物管理员领导保持多数,所有客户 大多数人都能正常运作。
-
如果现任动物管理员领导成为少数派,所有客户 断开连接,试着回去连接,直到之前 少数党成员已成功重新加入多数派 整体。
| 在我们目前的Jepsen测试中,我们无法将Zookeeper与分裂大脑分离 领导者要么处于多数派,要么处于少数派之间的情景,所以我们需要 多次运行测试以实现这种情况。 |
在以下图中,我们将状态机错误状态映射为错误表示状态机处于错误状态,而非
一种正常状态。请在解读星盘状态时记住这一点。 |
在第一个测试中,我们展示了当现有的Zookeeper 领导者 保持多数的五台机器中,三台继续保持现状。 下图展示了该测试:
在前图中:
-
第一个事件,
C,发送到所有机器,导致状态变化为S211. -
Jepsen的死敌导致脑部分裂,进而产生分裂 之
N1/N2/N5和N3/N4.节点N3/N4处于少数,且 节点N1/N2/N5构建一个新的健康多数。节点 大多数节点仍能正常运行,但节点存在少数 进入错误状态。 -
Jepsen修复网络,经过一段时间后,节点也被修复
N3/N4加入 回归集合并同步其分布状态。 -
最后,事件
K1发送给所有状态机以确保集合 正常工作。这种状态变化又导致另一个状态S21.
在第二个测试中,我们展示了当现有的动物管理员领袖 保持少数,所有机器都会出错。 下图展示了第二个测试:
在前图中:
-
第一个事件,
C,发送到所有机器,导致状态变化为S211. -
Jepsen的死敌导致脑部分裂,进而产生分裂 使得
Zookeeper领袖被保留在少数派中 实例与集合断开连接。 -
Jepsen修复了网络,过了一段时间,所有节点都加入了 回归集合并同步其分布状态。
-
最后,事件
K1发送给所有状态机以确保该集合 正常工作。这种状态变化又导致另一个状态S21.
开发者文档
本附录为可能的开发者提供了通用信息 想贡献意见,或者其他想了解州政府情况的人 机器运作或理解其内部概念。
状态机配置模型
StateMachineModel而其他相关的SPI类则是抽象的
在不同配置和工厂级别之间。这也允许
让别人更容易集成状态机。
正如下面的列表所示,你可以通过构建模型来实例化状态机 使用配置数据类,然后让工厂构建一个 状态机:
// 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();