写点什么

管理订单状态,该上状态机吗?轻量级状态机 COLA StateMachine 保姆级入门教程

作者:Zhendong
  • 2022 年 6 月 02 日
  • 本文字数:6912 字

    阅读完需:约 23 分钟

管理订单状态,该上状态机吗?轻量级状态机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 状态机设计的目标很明确,有两个核心理念:


  1. 简洁的仅支持状态流转的状态机,不需要支持嵌套、并行等高级玩法。

  2. 状态机本身需要是 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"));
复制代码


内部实现如下:



  1. 首先判断 COLA 状态机整个组件是否初始化完成。

  2. 通过 routeTransition 寻找是否有符合条件的状态流转。

  3. transition.transit 执行状态流转。


transition.transit 方法中:



检查本次流转是否符合 condition,符合,则执行对应的 action。

COLA 状态机实战

**PS:以下实战代码取自 COLA 官方仓库测试类

一、状态流转使用示例

  1. 从单一状态流转到另一个状态


@Testpublic 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()方法在下方就不再重复贴出了。


  1. 从多个状态流传到新的状态


@Testpublic 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);}
复制代码


  1. 状态内部触发流转


@Testpublic 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);}
复制代码


  1. 多线程测试并发测试


@Testpublic 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*/@Testpublic 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 图,实现状态机的可视化。(可以用图说话,和产品对比下状态实现的是否正确了。)

四、特殊使用示例

  1. 不满足状态流转条件时的处理


@Testpublic 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。相关源码在这里:



  1. 重复定义相同的状态流转


@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 检查这里,如下:



  1. 重复定义状态机


@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 是个十分合适的选择。


我是后端工程师,蛮三刀酱。


持续的更新原创优质文章,离不开你的点赞,转发和分享!


我的唯一技术公众号:后端技术漫谈

发布于: 刚刚阅读数: 3
用户头像

Zhendong

关注

公众号:后端技术漫谈、蛮三刀酱 2020.07.17 加入

公众号:后端技术漫谈、蛮三刀酱

评论

发布
暂无评论
管理订单状态,该上状态机吗?轻量级状态机COLA StateMachine保姆级入门教程_Java_Zhendong_InfoQ写作社区