管理订单状态,该上状态机吗?轻量级状态机COLA StateMachine保姆级入门教程
前言
在平常的后端项目开发中,状态机模式的使用其实没有大家想象中那么常见,笔者之前由于不在电商领域工作,很少在业务代码中用状态机来管理各种状态,一般都是手动get/set状态值。去年笔者进入了电商领域从事后端开发。电商领域,状态又多又复杂,如果仍然在业务代码中东一块西一块维护状态值,很容易陷入出了问题难于Debug,难于追责的窘境。
碰巧有个新启动的项目需要进行订单状态的管理,我着手将Spring StateMachine接入了进来,管理购物订单状态,不得不说,Spring StateMachine全家桶的文档写的是不错,并且Spring StateMachine也是有官方背书的。但是,它实在是太”重“了,想要简单修改一个订单的状态,需要十分复杂的代码来实现。具体就不在这里展开了,不然我感觉可以吐槽一整天。
说到底Spring StateMachine上手难度非常大,如果没有用来做重型状态机的需求,十分不推荐普通的小项目进行接入。
最最重要的是,由于Spring StateMachine状态机实例不是无状态的,无法做到线程安全,所以代码要么需要使用锁同步,要么需要用Threadlocal,非常的痛苦和难用。 例如下面的Spring StateMachine代码就用了重量级锁保证线程安全,在高并发的互联网应用中,这种代码留的隐患非常大。
private synchronized boolean sendEvent(Message<PurchaseOrderEvent> message, OrderEntity orderEntity) {
boolean result = false;
try {
stateMachine.start();
// 尝试恢复状态机状态
persister.restore(stateMachine, orderEntity);
// 执行事件
result = stateMachine.sendEvent(message);
// 持久化状态机状态
persister.persist(stateMachine, (OrderEntity) message.getHeaders().get("purchaseOrder"));
} catch (Exception e) {
log.error("sendEvent error", e);
} finally {
stateMachine.stop();
}
return result;
}
吃了一次亏后,我再一次在网上翻阅各种Java状态机的实现,有大的开源项目,也有小而美的个人实现。结果在COLA架构中发现了COLA还写了一套状态机实现。COLA的作者给我们提供了一个无状态的,轻量化的状态机,接入十分简单。并且由于无状态的特点,可以做到线程安全,支持电商的高并发场景。
COLA是什么?如果你还没听说过COLA,不妨看一看我之前的文章,传送门如下:
https://mp.weixin.qq.com/s/07i3FjcFrZ8rxBCACgeWVQ
如果你需要在项目中引入状态机,此时此刻,我会推荐使用COLA状态机。
COLA状态机介绍
COLA状态机是在Github开源的,作者也写了介绍文章:
https://blog.csdn.net/significantfrank/article/details/104996419
官方文章的前半部分重点介绍了DSL(Domain Specific Languages),这一部分比较抽象和概念化,大家感兴趣,可以前往原文查看。我精简一下DSL的主要含义:
什么是DSL? DSL是一种工具,它的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。
比如正则表达式,
/\d{3}-\d{3}-\d{4}/
就是一个典型的DSL,解决的是字符串匹配这个特定领域的问题。
文章的后半部分重点阐述了作者为什么要做COLA状态机?想必这也是读者比较好奇的问题。我帮大家精简一下原文的表述:
- 首先,状态机的实现应该可以非常的轻量,最简单的状态机用一个Enum就能实现,基本是零成本。
- 其次,使用状态机的DSL来表达状态的流转,语义会更加清晰,会增强代码的可读性和可维护性。
- 开源状态机太复杂: 就我们的项目而言(其实大部分项目都是如此)。我实在不需要那么多状态机的高级玩法:比如状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。
- 开源状态机性能差: 这些开源的状态机都是有状态的(Stateful)的,因为有状态,状态机的实例就不是线程安全的,而我们的应用服务器是分布式多线程的,所以在每一次状态机在接受请求的时候,都不得不重新build一个新的状态机实例。
所以COLA状态机设计的目标很明确,有两个核心理念:
- 简洁的仅支持状态流转的状态机,不需要支持嵌套、并行等高级玩法。
- 状态机本身需要是Stateless(无状态)的,这样一个Singleton Instance就能服务所有的状态流转请求了。
COLA状态机的核心概念如下图所示,主要包括:
State:状态
Event:事件,状态由事件触发,引起变化
Transition:流转,表示从一个状态到另一个状态
External Transition:外部流转,两个不同状态之间的流转
Internal Transition:内部流转,同一个状态之间的流转
Condition:条件,表示是否允许到达某个状态
Action:动作,到达某个状态之后,可以做什么
StateMachine:状态机
COLA状态机原理
这一小节,我们先讲几个COLA状态机最重要两个部分,一个是它使用的连贯接口,一个是状态机的注册和使用原理。如果你暂时对它的实现原理不感兴趣,可以直接跳过本小节,直接看后面的实战代码部分。
PS:讲解的代码版本为cola-component-statemachine 4.2.0-SNAPSHOT
下图展示了COLA状态机的源代码目录,可以看到非常的简洁。
1. 连贯接口 Fluent Interfaces
COLA状态机的定义使用了连贯接口Fluent Interfaces,连贯接口的一个重要作用是,限定方法调用的顺序。比如,在构建状态机的时候,我们只有在调用了from方法后,才能调用to方法,Builder模式没有这个功能。
下图中可以看到,我们在使用的时候是被严格限制的:
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
这是如何实现的?其实是使用了Java接口来实现。
2. 状态机注册和触发原理
这里简单梳理一下状态机的注册和触发原理。
用户执行如下代码来创建一个状态机,指定一个MACHINE_ID:
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);
COLA会将该状态机在StateMachineFactory类中,放入一个ConcurrentHashMap,以状态机名为key注册。
static Map<String /* machineId */, StateMachine> stateMachineMap = new ConcurrentHashMap<>();
注册好后,用户便可以使用状态机,通过类似下方的代码触发状态机的状态流转:
stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("1"));
内部实现如下:
- 首先判断COLA状态机整个组件是否初始化完成。
- 通过routeTransition寻找是否有符合条件的状态流转。
- transition.transit执行状态流转。
transition.transit方法中:
检查本次流转是否符合condition,符合,则执行对应的action。
COLA状态机实战
**PS:以下实战代码取自COLA官方仓库测试类
一、状态流转使用示例
- 从单一状态流转到另一个状态
@Test
public void testExternalNormal(){
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
Assert.assertEquals(States.STATE2, target);
}
private Condition<Context> checkCondition() {
return (ctx) -> {return true;};
}
private Action<States, Events, Context> doAction() {
return (from, to, event, ctx)->{
System.out.println(ctx.operator+" is operating "+ctx.entityId+" from:"+from+" to:"+to+" on:"+event);
};
}
可以看到,每次进行状态流转时,检查checkCondition(),当返回true,执行状态流转的操作doAction()。
后面所有的checkCondition()和doAction()方法在下方就不再重复贴出了。
- 从多个状态流传到新的状态
@Test
public void testExternalTransitionsNormal(){
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransitions()
.fromAmong(States.STATE1, States.STATE2, States.STATE3)
.to(States.STATE4)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID+"1");
States target = stateMachine.fireEvent(States.STATE2, Events.EVENT1, new Context());
Assert.assertEquals(States.STATE4, target);
}
- 状态内部触发流转
@Test
public void testInternalNormal(){
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.internalTransition()
.within(States.STATE1)
.on(Events.INTERNAL_EVENT)
.when(checkCondition())
.perform(doAction());
StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID+"2");
stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
States target = stateMachine.fireEvent(States.STATE1, Events.INTERNAL_EVENT, new Context());
Assert.assertEquals(States.STATE1, target);
}
- 多线程测试并发测试
@Test
public void testMultiThread(){
buildStateMachine("testMultiThread");
for(int i=0 ; i<10 ; i++){
Thread thread = new Thread(()->{
StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
Assert.assertEquals(States.STATE2, target);
});
thread.start();
}
for(int i=0 ; i<10 ; i++) {
Thread thread = new Thread(() -> {
StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT4, new Context());
Assert.assertEquals(States.STATE4, target);
});
thread.start();
}
for(int i=0 ; i<10 ; i++) {
Thread thread = new Thread(() -> {
StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get("testMultiThread");
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT3, new Context());
Assert.assertEquals(States.STATE3, target);
});
thread.start();
}
}
由于COLA状态机时无状态的状态机,所以性能是很高的。相比起来,SpringStateMachine由于是有状态的,就需要使用者自行保证线程安全了。
二、多分支状态流转示例
/**
* 测试选择分支,针对同一个事件:EVENT1
* if condition == "1", STATE1 --> STATE1
* if condition == "2" , STATE1 --> STATE2
* if condition == "3" , STATE1 --> STATE3
*/
@Test
public void testChoice(){
StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, Context> builder = StateMachineBuilderFactory.create();
builder.internalTransition()
.within(StateMachineTest.States.STATE1)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition1())
.perform(doAction());
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE2)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition2())
.perform(doAction());
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE3)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition3())
.perform(doAction());
StateMachine<StateMachineTest.States, StateMachineTest.Events, Context> stateMachine = builder.build("ChoiceConditionMachine");
StateMachineTest.States target1 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("1"));
Assert.assertEquals(StateMachineTest.States.STATE1,target1);
StateMachineTest.States target2 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("2"));
Assert.assertEquals(StateMachineTest.States.STATE2,target2);
StateMachineTest.States target3 = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new Context("3"));
Assert.assertEquals(StateMachineTest.States.STATE3,target3);
}
可以看到,编写一个多分支的状态机也是非常简单明了的。
三、通过状态机反向生成PlantUml图
没想到吧,还能通过代码定义好的状态机反向生成plantUML图,实现状态机的可视化。(可以用图说话,和产品对比下状态实现的是否正确了。)
四、特殊使用示例
- 不满足状态流转条件时的处理
@Test
public void testConditionNotMeet(){
StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE2)
.on(StateMachineTest.Events.EVENT1)
.when(checkConditionFalse())
.perform(doAction());
StateMachine<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> stateMachine = builder.build("NotMeetConditionMachine");
StateMachineTest.States target = stateMachine.fireEvent(StateMachineTest.States.STATE1, StateMachineTest.Events.EVENT1, new StateMachineTest.Context());
Assert.assertEquals(StateMachineTest.States.STATE1,target);
}
可以看到,当checkConditionFalse()执行时,永远不会满足状态流转的条件,则状态不会变化,会直接返回原来的STATE1。相关源码在这里:
- 重复定义相同的状态流转
@Test(expected = StateMachineException.class)
public void testDuplicatedTransition(){
StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE2)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition())
.perform(doAction());
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE2)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition())
.perform(doAction());
}
会在第二次builder执行到on(StateMachineTest.Events.EVENT1)函数时,抛出StateMachineException异常。抛出异常在on()的verify检查这里,如下:
- 重复定义状态机
@Test(expected = StateMachineException.class)
public void testDuplicateMachine(){
StateMachineBuilder<StateMachineTest.States, StateMachineTest.Events, StateMachineTest.Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(StateMachineTest.States.STATE1)
.to(StateMachineTest.States.STATE2)
.on(StateMachineTest.Events.EVENT1)
.when(checkCondition())
.perform(doAction());
builder.build("DuplicatedMachine");
builder.build("DuplicatedMachine");
}
会在第二次build同名状态机时抛出StateMachineException异常。抛出异常的源码在状态机的注册函数中,如下:
结语
为了不把篇幅拉得过长,在这里无法详细地横向对比几大主流状态机(Spring Statemachine,Squirrel statemachine等)和COLA的区别,不过基于笔者在Spring Statemachine踩过的深坑,目前来看,COLA状态机的简洁设计适合用在订单管理等小型状态机的维护,如果你想要在你的项目中接入状态机,又不需要嵌套、并行等高级玩法,那么COLA是个十分合适的选择。
我是后端工程师,蛮三刀酱。
持续的更新原创优质文章,离不开你的点赞,转发和分享!
我的唯一技术公众号:后端技术漫谈
管理订单状态,该上状态机吗?轻量级状态机COLA StateMachine保姆级入门教程的更多相关文章
- OsharpNS轻量级.net core快速开发框架简明入门教程-从零开始启动Osharp
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-基于Osharp实现自己的业务功能
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-Osharp.Redis使用
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-代码生成器的使用
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-切换数据库(从SqlServer改为MySql)
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-Osharp.Hangfire使用
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-Osharp.Permissions使用
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- OsharpNS轻量级.net core快速开发框架简明入门教程-多上下文配置(多个数据库的使用)
OsharpNS轻量级.net core快速开发框架简明入门教程 教程目录 从零开始启动Osharp 1.1. 使用OsharpNS项目模板创建项目 1.2. 配置数据库连接串并启动项目 1.3. O ...
- 《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(6)-Fiddler状态面板详解
1.简介 按照从上往下,从左往右的计划,今天就轮到介绍和分享Fiddler的状态面板了. 2.状态面板概览 Fiddler的状态面板概览,如下图所示: 3.状态面板详解 Fiddler底端状态栏面板详 ...
随机推荐
- Python的组合数据类型
""" Python的组合类型: 序列类型:元素之间存在先后关系,可以通过索引来访问 列表: 元组: 字符串: 映射类型:用键值来表示数据 字典: 集合类型:元素是无序的 ...
- LC-977
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序. 示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,1 ...
- 群晖下虚拟机编译部署WOW服务端TrinityCore
前言 前几天突然想玩WOW了,但是我是一个特别轻度的玩家,以前点卡的时候,我就是上去一个人做做任务,跑跑地图,不怎么玩副本和PVP,现在让我花钱充月卡,不太现实,没那个时间玩,所以,就考虑玩个私服,但 ...
- 计算属性、侦听属性、局部与全局组件使用、组件通信(父子互传)、ref属性、动态组件和keep-alive、插槽
今日内容概要 计算属性 侦听属性 局部组件和全局组件 组件通信之父传子 组件通信之子传父 ref属性(组件间通信) 动态组件和keep-alive 插槽 内容详细 1.计算属性 # 插值的普通函数,只 ...
- go 中 select 源码阅读
深入了解下 go 中的 select 前言 1.栗子一 2.栗子二 3.栗子三 看下源码实现 1.不存在 case 2.select 中仅存在一个 case 3.select 中存在两个 case,其 ...
- 基于 POI 封装 ExcelUtil 精简的 Excel 导入导出
注 本文是使用 org.apache.poi 进行一次简单的封装,适用于大部分 excel 导入导出功能.过程中可能会用到反射,如若有对于性能有极致强迫症的同学,看看就好. 序 由于 poi 本身只是 ...
- 今天遇到 Could not determine type for: java.util.List
今天遇到 Could not determine type for: java.util.List 用hibernate 映射好好的竟然出现这个问题 以前也遇到过,但不知道怎么给解决了,今天又遇到了, ...
- MQTT物联网通讯协议入门
目录 一.MQTT协议概念 发布/订阅机制 MQTT客户端 Broker代理(服务器) MQTT消息结构 二.MQTT协议实现原理 MQTT连接 MQTT消息发布 MQTT订阅机制 MQTT订阅确认 ...
- springmvc05-json交互处理
什么是json: JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛. *采用完全独立于编程语言的文本格式来存储和表示数据 ...
- SmartIDE v0.1.16 已经发布 - 支持阿里&蚂蚁开源的国产 IDE OpenSumi
SmartIDE v0.1.16 (Build 3137) 已经在2022年4月19日发布到稳定版通道,我们在这个版本中增加了阿里和蚂蚁发布的国产IDE OpenSumi的支持,以及其他一些改进.Sm ...