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

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

前言

在平常的后端项目开发中,状态机模式的使用其实没有大家想象中那么常见,笔者之前由于不在电商领域工作,很少在业务代码中用状态机来管理各种状态,一般都是手动get/set状态值。去年笔者进入了电商领域从事后端开发。电商领域,状态又多又复杂,如果仍然在业务代码中东一块西一块维护状态值,很容易陷入出了问题难于Debug,难于追责的窘境。

碰巧有个新启动的项目需要进行订单状态的管理,我着手将Spring StateMachine接入了进来,管理购物订单状态,不得不说,Spring StateMachine全家桶的文档写的是不错,并且Spring StateMachine也是有官方背书的。但是,它实在是太”重“了,想要简单修改一个订单的状态,需要十分复杂的代码来实现。具体就不在这里展开了,不然我感觉可以吐槽一整天。

说到底Spring StateMachine上手难度非常大,如果没有用来做重型状态机的需求,十分不推荐普通的小项目进行接入。

最最重要的是,由于Spring StateMachine状态机实例不是无状态的,无法做到线程安全,所以代码要么需要使用锁同步,要么需要用Threadlocal,非常的痛苦和难用。 例如下面的Spring StateMachine代码就用了重量级锁保证线程安全,在高并发的互联网应用中,这种代码留的隐患非常大。

private synchronized boolean sendEvent(Message 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 builder = StateMachineBuilderFactory.create(); builder.externalTransition() .from(States.STATE1) .to(States.STATE2) .on(Events.EVENT1) .when(checkCondition()) .perform(doAction());

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

    2. 状态机注册和触发原理

    这里简单梳理一下状态机的注册和触发原理。

    用户执行如下代码来创建一个状态机,指定一个MACHINE_ID:

    StateMachine stateMachine = builder.build(MACHINE_ID);

    COLA会将该状态机在StateMachineFactory类中,放入一个ConcurrentHashMap,以状态机名为key注册。

    static Map 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官方仓库测试类

    一、状态流转使用示例

  • 从单一状态流转到另一个状态
  • @Testpublic void testExternalNormal(){ StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransition() .from(States.STATE1) .to(States.STATE2) .on(Events.EVENT1) .when(checkCondition()) .perform(doAction()); StateMachine stateMachine = builder.build(MACHINE_ID); States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context()); Assert.assertEquals(States.STATE2, target);}private Condition checkCondition() { return (ctx) -> {return true;};}private Action 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()方法在下方就不再重复贴出了。

  • 从多个状态流传到新的状态
  • @Testpublic void testExternalTransitionsNormal(){ StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransitions() .fromAmong(States.STATE1, States.STATE2, States.STATE3) .to(States.STATE4) .on(Events.EVENT1) .when(checkCondition()) .perform(doAction()); StateMachine stateMachine = builder.build(MACHINE_ID+”1″); States target = stateMachine.fireEvent(States.STATE2, Events.EVENT1, new Context()); Assert.assertEquals(States.STATE4, target);}

  • 状态内部触发流转
  • @Testpublic void testInternalNormal(){ StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.internalTransition() .within(States.STATE1) .on(Events.INTERNAL_EVENT) .when(checkCondition()) .perform(doAction()); StateMachine 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);}

  • 多线程测试并发测试
  • @Testpublic void testMultiThread(){ buildStateMachine(“testMultiThread”); for(int i=0 ; i{ StateMachine 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 { StateMachine 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 { StateMachine 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 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 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图,实现状态机的可视化。(可以用图说话,和产品对比下状态实现的是否正确了。)

    四、特殊使用示例

  • 不满足状态流转条件时的处理
  • @Testpublic void testConditionNotMeet(){ StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransition() .from(StateMachineTest.States.STATE1) .to(StateMachineTest.States.STATE2) .on(StateMachineTest.Events.EVENT1) .when(checkConditionFalse()) .perform(doAction()); StateMachine 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 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 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是个十分合适的选择。

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

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

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

    – END –

    郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
    (0)
    用户投稿
    上一篇 2022年7月1日
    下一篇 2022年7月1日

    相关推荐

    • 网站引流推广怎么做(怎样引流推广)

      推广引流,永远都是一个具有争议的话题。 推广引流方法对不对,直接影响效果。 如果你对推广引流有疑问的地方,这篇文章可以为你解惑并且能带给你一些推广引流方法的新思路。 让我们一起开始…

      2022年10月19日
    • 苹果手机上实现微信双开,完美解决iPhone ios系统微信分身

      #谣零零计划# 微信是手机上必不可少的社交软件,安卓手机有自带的分身软件可以多开微信1-2个,但是苹果就不一样了,由于安全性的保障,苹果还没有出官方的渠道,这就让工作号和私人号的用…

      2022年6月27日
    • 计算机专业丨学完一年编程还是不会写代码,问题到底出在哪?

      这几年的教学工作做下来,发现无论是计算机专业的学生,还是把编程作为公修课的其他专业学生,很多人内心都有一个灵魂拷问——为什么我认真听课,认真看书书,几个月甚至一年下来但还是写不好代…

      2022年6月29日
    • 戚薇怀二胎巧遮孕肚!穿粉色蓬蓬裙少女感十足,晒上班照状态超好

      戚薇怀上二胎之后,整个人都是充满活力的,和老公李承铉也是十分开心,两人还特地在自己的社交平台上官宣二胎的好消息,即便是怀二胎,戚薇也没有在家待产休息,反而是更加努力的工作了。 在近…

      2022年6月13日
    • Spring源码之容器的功能扩展和refresh方法解析

      容器的功能扩展和refresh方法解析 在之前文章中我们了解了关于Spring中bean的加载流程,并一直使用BeanFactory接口以及它的默认实现类XmlBeanFactor…

      2022年6月20日
    • 小韭菜日记(27) 自我价值与知行合一

      上周末去参加了卓越父母的培训,对我启发很大。通过这次培训,我发现人生最重要的就是自我。我第一次知道很多人其实一直活在自己的面具下面,他们没法摘下自己的面具。所以他们每天生活很累,别…

      2022年8月22日
    • 集泰股份:拟定增募资不超2.8亿元用于新能源密封胶等项目

      集微网消息,6月27日晚间,集泰股份发布公告,拟非公开发行股票募集资金总额不超过2.8亿元,用于年产2万吨乙烯基硅油、2万吨新能源密封胶、0.2万吨核心助剂建设项目。 其中,乙烯基…

      2022年6月29日
    • 为什么有些晚上月亮看起来更大?

      先说结论:月球的大小始终相同,它从来没有变得更大或更小。那为什么我们看到它有时大有时小呢? 不同时候的月亮 主要有两个因素在起作用:一个是次要因素,另一个是主要因素。 次要因素是月…

      2022年8月16日
    • 《中餐厅6》出圈理由:尹正的脑洞,黄晓明的脸蛋,殷桃的衣品

      今年暑期档的各大综艺都算是出彩,其中芒果台确实没少在这方面下功夫,专攻年轻人市场的《密室大逃脱第四季》。 本就有杨幂这位顶流保证热度,又是有彭昱畅、许凯、张若昀三大胆小飞行嘉宾直接…

      2022年8月30日
    • 爆料消息显示,iPhone14系列8月将开启量产

      近日,有媒体发布消息表示,iPhone14系列8月将开启量产,三星已经拿到8000元的屏幕面板订单,同时,京东方也将提供部分屏幕订单。九月发布的话,时间还是非常充足的,预计会非常火…

      2022年6月28日

    联系我们

    联系邮箱:admin#wlmqw.com
    工作时间:周一至周五,10:30-18:30,节假日休息