管理订单状态,该用上状态机吗?

简介: 说到底Spring StateMachine上手难度非常大,如果没有用来做重型状态机的需求,十分不推荐普通的小项目进行接入。最最重要的是,由于Spring StateMachine状态机实例不是无状态的,无法做到线程安全,所以代码要么需要使用锁同步,要么需要用Threadlocal,非常的痛苦和难用。 例如下面的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状态机介绍



我精简一下DSL的主要含义:

什么是DSL? DSL是一种工具,它的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。

比如正则表达式,/\d{3}-\d{3}-\d{4}/就是一个典型的DSL,解决的是字符串匹配这个特定领域的问题。

文章的后半部分重点阐述了作者为什么要做COLA状态机?想必这也是读者比较好奇的问题。我帮大家精简一下原文的表述:

  • 首先,状态机的实现应该可以非常的轻量,最简单的状态机用一个Enum就能实现,基本是零成本。
  • 其次,使用状态机的DSL来表达状态的流转,语义会更加清晰,会增强代码的可读性和可维护性
  • 开源状态机太复杂:就我们的项目而言(其实大部分项目都是如此)。我实在不需要那么多状态机的高级玩法:比如状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等
  • 开源状态机性能差:这些开源的状态机都是有状态的(Stateful)的,因为有状态,状态机的实例就不是线程安全的,而我们的应用服务器是分布式多线程的,所以在每一次状态机在接受请求的时候,都不得不重新build一个新的状态机实例。

image.png

所以COLA状态机设计的目标很明确,有两个核心理念:

  1. 简洁的仅支持状态流转的状态机,不需要支持嵌套、并行等高级玩法。
  2. 状态机本身需要是Stateless(无状态)的,这样一个Singleton Instance就能服务所有的状态流转请求了。

COLA状态机的核心概念如下图所示,主要包括:

State:状态 Event:事件,状态由事件触发,引起变化 Transition:流转,表示从一个状态到另一个状态 External Transition:外部流转,两个不同状态之间的流转 Internal Transition:内部流转,同一个状态之间的流转 Condition:条件,表示是否允许到达某个状态 Action:动作,到达某个状态之后,可以做什么 StateMachine:状态机


COLA状态机原理



这一小节,我们先讲几个COLA状态机最重要两个部分,一个是它使用的连贯接口,一个是状态机的注册和使用原理。如果你暂时对它的实现原理不感兴趣,可以直接跳过本小节,直接看后面的实战代码部分。

下图展示了COLA状态机的源代码目录,可以看到非常的简洁。

image.pngimage.png

1. 连贯接口 Fluent Interfaces

COLA状态机的定义使用了连贯接口Fluent Interfaces,连贯接口的一个重要作用是,限定方法调用的顺序。比如,在构建状态机的时候,我们只有在调用了from方法后,才能调用to方法,Builder模式没有这个功能。

下图中可以看到,我们在使用的时候是被严格限制的:

image.png

StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE2)
                .on(Events.EVENT1)
                .when(checkCondition())
                .perform(doAction());

这是如何实现的?其实是使用了Java接口来实现。

image.png


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"));

内部实现如下:

image.png

  1. 首先判断COLA状态机整个组件是否初始化完成。
  2. 通过routeTransition寻找是否有符合条件的状态流转。
  3. transition.transit执行状态流转。

transition.transit方法中:

image.png

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


COLA状态机实战



一、状态流转使用示例

  1. 从单一状态流转到另一个状态
@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()方法在下方就不再重复贴出了。

  1. 从多个状态流传到新的状态
@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);
}
  1. 状态内部触发流转
@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);
}
  1. 多线程测试并发测试
@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图

image.png

没想到吧,还能通过代码定义好的状态机反向生成plantUML图,实现状态机的可视化。(可以用图说话,和产品对比下状态实现的是否正确了。)


四、特殊使用示例

  1. 不满足状态流转条件时的处理
@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。相关源码在这里:

image.png


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

image.png

  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异常。抛出异常的源码在状态机的注册函数中,如下:

image.png


结语



为了不把篇幅拉得过长,在这里无法详细地横向对比几大主流状态机(Spring Statemachine,Squirrel statemachine等)和COLA的区别,不过基于笔者在Spring Statemachine踩过的深坑,目前来看,COLA状态机的简洁设计适合用在订单管理等小型状态机的维护,如果你想要在你的项目中接入状态机,又不需要嵌套、并行等高级玩法,那么COLA是个十分合适的选择。

相关文章
|
uml
状态机
首先需要考虑涉及到哪些状态节点和哪些事件,如何方便状态节点的获取、状态节点如何串联起来呢?串联的方式下,如何拿到下一个状态节点?如果基于角色,如何实现? 我们知道工作流可以实现基于角色进行流程的流转,但是此时我们涉及到事件和状态,会出现多个分支,如果使用工作流实现,流程处理上,比如activiti上,可能比较复杂,因此考虑比较轻量级的状态机来实现的话,相对来说要方便一些。
1700 0
状态机
|
存储 Java API
阿里高级技术专家谈开源DDD框架:COLA4.1,分离架构和组件(下)
阿里高级技术专家谈开源DDD框架:COLA4.1,分离架构和组件(下)
11729 8
阿里高级技术专家谈开源DDD框架:COLA4.1,分离架构和组件(下)
|
消息中间件 NoSQL Kafka
订单超时取消的11种方式(非常详细清楚)
订单超时取消的11种方式(非常详细清楚)
8740 5
订单超时取消的11种方式(非常详细清楚)
|
传感器 数据可视化 JavaScript
状态机(State Machines):理解、设计和应用有限状态机
状态机(State Machines)是一种强大的计算模型和设计工具,用于建模和控制有限状态的系统和行为。无论是在软件开发、自动化控制、游戏设计还是其他领域,状态机都发挥着关键作用。本博客将深入探讨状态机的概念、工作原理以及如何在不同应用中设计和应用它们。
10398 78
|
设计模式 Java Spring
Spring状态机为啥如此牛逼?
【2月更文挑战第7天】
1716 0
|
消息中间件 Docker 索引
【一文解读】阿里自研开源核心搜索引擎 Havenask简介及发展历史
本次分享内容为Havenask的简介及发展历史,由下面五个部分组成(Havenask整体介绍、名词解释、架构、代码结构、编译与部署),希望可以帮助大家更好了解和使用Havenask。
73358 0
【一文解读】阿里自研开源核心搜索引擎 Havenask简介及发展历史
|
Java 测试技术 开发者
COLA-statemachine事务失效踩坑
cola-statemachine是阿里开源项目COLA中的轻量级状态机组件。最大的特点是无状态、采用纯Java实现,用Fluent Interface(连贯接口)定义状态和事件,可用于管理状态转换场景。比如:订单状态、支付状态等简单有限状态场景。在实际使用的过程中笔者曾发现状态机内事务不生效的问题,经过排查得到解决,以此记录一下
2383 1
|
XML 数据可视化 安全
给 DSL 开个脑洞:无状态的状态机
什么是 DSL ?DSL 是一种工具,其核心价值在于提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。本文将通过实现一个状态机引擎来看清 DSL 的本质,介绍状态机的核心模型和 Fluent 接口,并解决状态机的性能问题。
2095 0
给 DSL 开个脑洞:无状态的状态机
|
存储 Java 数据库
Spring Boot与分布式事务的最佳实践
Spring Boot与分布式事务的最佳实践