得物商品状态体系介绍

一、得物的商品体系

目前得物的商品分为三种类型,分别是:新品、商品、草稿。但是只有商品是可售卖的,新品和草稿都不是可售卖的。

新品有很多种创建的渠道,商品可以由新品选品通过后由系统自动生成,也可以由运营直接创建。而商品草稿是在商品被编辑后创建而来,草稿在更新审核通过后,会重新覆盖已有的商品信息。

新品

新品是企业卖家或个人卖家或者 ISV 等渠道申请的一种不可售卖的商品,需要运营选品审核、商研审核通过后,才会变成可售卖的商品,精简后的新品申请的流程大致如下图所示:

商品

新品在审核通过后,就变成了商品,商品的状态有上架、下架、待审核等多种,只有上架状态的商品是可售卖的。

未上架的商品有两种情况,一种是待补全信息,一种是待审核,正常审核通过的商品如果没有特殊条件,会自动上架。

第 1 种待补全的商品,这种商品是新品在运营选品通过后,就进入了商品池,需要补全其他信息,然后再由商研后置审核通过后进行上架。

第 2 种待审核的商品,这种商品是在原来上架状态的商品被下架并且编辑之后,等待审核。

待审核的商品还未上架过,所以没有草稿,如果编辑该商品会直接修改商品库里的信息。

如果已经上架了,再来编辑商品,则会先生成草稿,草稿保存在草稿库中,此时草稿的修改不会影响原商品。

新品上新的流程在迭代过程中也发生了变化:

1、现有 SPU 管理可以跳过商研审核,直接创建商品上架,流程上是将商研审核后置了;

2、得物运营自挖品,需要运营先在新品中提报,经过选品和商研审核后,再去 SPU 管理补全商品资料再审核上架。新品提报和补全资料这两个流程希望可以进行合并,节约上架时效和上架成本。

草稿

草稿是在商品的基础上编辑之后,生成的一个副本,草稿可以反复编辑。

在管理后台的页面上体现为,如果有"草稿"两个字,则说明这条记录是一个草稿,如果有"编辑"两个字,则说明这条记录是一个商品。

二、得物商品状态流转

目前得物的商品状态共有:下架、上架、待补全、待审核、审核通过等等数十种状态。

当对商品进行编辑时,会创建一条草稿记录,记录商品修改后的副本信息,保存在草稿库中,其中草稿的状态和商品原本的状态是隔离的,草稿的状态变更不会影响商品的状态。

精简后的各状态之间的流转如下图所示:

从上图可以看出,商品的状态已经相当丰富,状态之间的流转也是错综复杂,并且还涉及到不同的商品类型之间的流转。从系统后续的稳定性和可维护性来看,确实到了需要引入状态机来维护商品状态流转的时机了。

三、状态机图

状态机图是一种行为图,它通过有限的状态转换来表示系统中的某些行为。除此之外状态机图也可以用来表示系统的某种协议状态。UML 2.4 中定义的两种状态机是:行为状态机和协议状态机。具体可以参考 UML State Machine 中的定义。

核心组件

大体上状态机有以下几个核心的组件:

  • 状态
  • 事件
  • 流转
  • 条件
  • 动作

通过这些组件共同来形成一个完整的状态机:

如下图所示,表示有两个状态,StateA 和 StateB,两个状态之间通过 EventA 和 EventB 事件进行流转。在状态扭转之前需要满足一定的条件,条件满足后即可执行状态流转,并可执行状态流转后的动作。

状态类型

在 UML 的定义中,状态有三种类型:简单状态、组合状态、子状态机状态。

其中简单状态、组合状态比较好理解,如下图所示,组合状态将多个简单状态进行组合封装,形成一个新的复杂的状态,内部状态和它的内部内容被定义它们的状态机所包含,如下图所示:

但是子状态机状态(以下我们用子机状态来表示)相对比较复杂,子机状态在语义上等同于组合状态。子机状态机的区域是组合状态的区域。

进入、退出和其他行为动作以及内部转换被定义为状态的一部分。子机状态是一种分解机制,它允许对公共行为进行分解并复用。子状态机是一个状态机定义可以被多次复用的方式。它也需要将进入和离开迁移绑定到内部顶点上,这一点与封装组合状态类似。

子机状态最重要的作用就是封装和复用,概念理解起来比较晦涩难懂,下面我们用一张图来描述:

虽然 UML 在状态机的定义中定义了这么多种状态,但实际上我们只需要简单状态就够用了。

四、状态机选型介绍

开源的状态机引擎有很多,目前在 Github 上的 Top 2 状态机实现中,一个是 Spring StateMachine,一个是 Squirrel StateMachine。他们的优点是功能很完备,缺点也是功能很完备。

就我们的项目而言,不需要那么多状态机的高级玩法,其实大部分项目都是如此:比如状态的嵌套(nested state),状态的并行(parallel,fork,join)、子状态机等等。

网上已经有非常多的状态机选型的文章了,这里不再长篇赘述,只做简单的介绍。

Enum StateMachine

在看开源的状态机引擎之前,我们先看一下,通过枚举实现一个状态机的最简单方式。

枚举类型因为自身的线程安全性保障和高可读性特性,是简单状态机的首选。

首先我们定义一个枚举,表示商品的状态,并在枚举中定义一个状态流转的方法,其中状态流转的抽象方法中接收 3 个参数:

  • 期望流转到的目标状态
  • 状态流转的条件
  • 状态流转后的动作
csharp 复制代码
public interface StateCondition {
    // 检查是否能流转到目标状态
    boolean check(CommodityState target);
}

public interface StateAction {
    void doAction();
}

状态机的枚举定义如下:

typescript 复制代码
public enum CommodityState {
    // 待审核
    TO_AUDIT {
        @Override
        StateCondition getCondition() {return new ToAuditStateCondition();}
        @Override
        StateAction getAction() {return new ToAuditStateAction();}
    },
    // 已上架
    ON_SHELF {
        @Override
        StateCondition getCondition() {return new OnShelfStateCondition();}
        @Override
        StateAction getAction() {return new OnShelfStateAction();}
    },
    // 已下架
    OFF_SHELF {
        @Override
        StateCondition getCondition() {return new OffShelfStateCondition();}
        @Override
        StateAction getAction() {return new OffShelfStateAction();}
    };
    boolean transition(CommodityState target) {
        StateCondition condition = getCondition();
        if (condition.check(target)) {
            StateAction action = getAction();
            action.doAction();
            return true;
        }
        throw new IllegalArgumentException("当前状态不符合流转条件");
    }
    abstract StateCondition getCondition();
    abstract StateAction getAction();
}

具体的条件检查和执行的动作,都定义到每个状态具体的实现类中。

Spring StateMachine

Spring StateMachine 是 Spring 官方提供的状态机实现。

先从状态机的定义入手,StateMachine<States, Events>,其中:

  • StateMachine:状态机模型
  • State:S-状态,一般定义为一个枚举类,如创建、待风控审核、待支付等状态
  • Event:E-事件,同样定义成一个枚举类,如订单创建、订单审核、支付等,代表一个动作。一个状态机的定义就由这两个主要的元素组成,状态及对对应的事件(动作)。

Spring StateMachine 中的相关概念:

  • Transition: 节点,是组成状态机引擎的核心
  • Source:节点的当前状态
  • Target:节点的目标状态
  • Event:触发节点从当前状态到目标状态的动作
  • Guard:起校验功能,一般用于校验是否可以执行后续 Action
  • Action:用于实现当前节点对应的业务逻辑处理

以下是一些核心组件:

Spring StateMachine 的核心实现:

对于节点配置,可以看个简单的例子:

scss 复制代码
builder.configureTransitions()  // 配置节点
// 表示source target两种状态不同
.withExternal()   
// 当前节点状态
.source(SOURCE)  
// 目标节点状态
.target(TARGET)  
// 导致当前变化的动作/事件
.event(BizOrderStatusChangeEventEnum.EVT_CREATE)  
// 执行当前状态变更导致的业务逻辑处理,以及出异常时的处理
.action(orderCreateAction, errorHandlerAction);

其中有几种可选的类型:

  • WithExternal 是当 Source 和 Target 不同时的写法,如上例子。
  • WithInternal 当 Source 和 Target 相同时的串联写法,比如付款失败时,付款前及付款后都是待付款状态。
  • WithChoice 当执行一个动作,可能导致多种结果时,可以选择使用 Choice+Guard 来跳转。

更详细的进行 Spring 状态机的配置,可以参考这篇文章:www.jianshu.com/p/b0c9e4f9d...

Squirrel StateMachine

Squirrel-Foundation 是一款很优秀的开源产品,推荐大家阅读以下它的源码。相较于 Spring statemachine,Squirrel 的实现更为轻量,设计域也很清晰,对应的文档以及测试用例也很丰富。

核心组件:

Squirrel StateMachine 的核心实现:

Squirrel 的事件处理模型与 Spring-Statemachine 比较类似,Squirrel 的事件执行器的作用点粒度更细,通过预处理,将一个状态迁移分解成 Exit Trasition Entry 这三个 Action Event,再递交给执行器分别执行(这个设计挺不错)。

怎样配置并使用 Squirrel StateMachine,可以参考这篇文章:blog.csdn.net/footless_bi...

Cola StateMachine

开源状态机都是有状态的(Stateful)的,有状态意味着多线程并发情况下如果是单个实例就容易出现线程安全问题。

如今我们的系统普遍都是分布式部署,不得不考虑多线程的问题,因为每来一个请求就需要创建一个状态机实例(per statemachine per request)。如果某些状态机它的构建过程很复杂,并且当下 QPS 又很高的话,往往会造成系统的性能瓶颈。

为此阿里出了一个开源的状态机:Cola-StateMachine

当时他们团队也想搞个状态机来减负,经过深思熟虑、不断类比之后他们考虑自研。希望能设计出一款功能相对简单、性能良好的开源状态机;最后命名为 Cola-ComPonent-Statemachine。

Cola-StateMachine 最重要的特点是,状态机的设计是无状态的,并且内部实现了 DSL 语法,通过流式 API 限定了方法调用的顺序。

分析一下市面上的开源状态机引擎,不难发现,它们之所以有状态,主要是在状态机里面维护了两个状态:初始状态(Initial State)和当前状态(Current State),如果我们能把这两个实例变量去掉的话,就可以实现无状态,从而实现一个状态机只需要有一个 Instance 就够了。

关键是这两个状态可以不要吗?当然可以,唯一的副作用是,我们没办法获取到状态机 Instance 的 Current State。然而,我也不需要知道,因为我们使用状态机,仅仅是接受一下 Source State,Check 一下 Condition,Execute 一下 Action,然后返回 Target State 而已。它只是实现了一个状态流转的 DSL 表达,仅此而已,全程操作完全可以是无状态的。

具体举例如下:

scss 复制代码
// 构建一个状态机(生产场景下,生产场景可以直接初始化一个Bean)
StateMachineBuilder<StateMachineTest.ApplyStates, StateMachineTest.ApplyEvents, Context> 
builder = StateMachineBuilderFactory.create();
// 外部流转(两个不同状态的流转)
builder.externalTransition()
.from(SOURCE)//原来状态
.to(TARGET)//目标状态
.on(EVENT1)//基于此事件触发
.when(checkCondition1())//前置过滤条件
.perform(doAction());//满足条件,最终触发的动作

更详细的介绍 Cola StateMachine 的资料,可以参考作者的介绍:blog.csdn.net/significant...

五、状态机性能评测

本次对比的是 Spring StateMachine 和 Cola StateMachine 的性能,为了尽量避免其他逻辑的影响,我在 Action 和 Condition 的实现类中,均是空实现,保证只评测两个框架本身的性能。

本次评测的两个框架的版本如下:

xml 复制代码
<dependency>
    <groupId>com.alibaba.cola</groupId>
    <artifactId>cola-component-statemachine</artifactId>
    <version>4.0.1</version>
</dependency>
xml 复制代码
<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
    <version>3.2.1</version>
</dependency>

准备测试代码

Spring StateMachine

将主要的代码都封装到两个类中:SpuStateMachineConfig 和 SpuStateMachineService

首先是 SpuStateMachineConfig,主要是对 Spring StateMachine 进行配置。

java 复制代码
/**
 * 状态机 核心配置
 */
@Configuration
public class SpuStateMachineConfig extends EnumStateMachineConfigurerAdapter<SpuStatesEnum, SpuEventsEnum> {

    public final static String DEFAULT_MACHINEID = "spring/machine/default/machineid";

    private final SpuStateMachinePersist spuStateMachinePersist = new SpuStateMachinePersist();

    private final StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> stateMachinePersister = new DefaultStateMachinePersister<>(spuStateMachinePersist);

    private final DefaultSpuGuard defaultSpuGuard = new DefaultSpuGuard();

    private final DefaultSpuErrorAction defaultSpuErrorAction = new DefaultSpuErrorAction();

    private final DefaultSpuSuccessAction defaultSpuSuccessAction = new DefaultSpuSuccessAction();

    private final SpuCreateDraftSuccessAction spuCreateDraftSuccessAction = new SpuCreateDraftSuccessAction();

    private final SpuCancelDraftSuccessAction spuCancelDraftSuccessAction = new SpuCancelDraftSuccessAction();


    public StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> getSpuMachinePersister() {
        return stateMachinePersister;
    }

    @Bean
    public StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> spuMachinePersister() {
        return getSpuMachinePersister();
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
        configMachineId(config, DEFAULT_MACHINEID);
    }

    @Override
    public void configure(StateMachineStateConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
        configureStates(config);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<SpuStatesEnum, SpuEventsEnum> transitions) throws Exception {
        configureTransitions(transitions);
    }

    @Bean(name = "spuStateMachineFactory")
    public StateMachineFactory<SpuStatesEnum, SpuEventsEnum> spuStateMachineFactory() throws Exception {
        StateMachineConfigBuilder<SpuStatesEnum, SpuEventsEnum> configBuilder = new StateMachineConfigBuilder<SpuStatesEnum, SpuEventsEnum>();
        // 通过apply方法将Configurer设置进去,this正好实现了
        // 也可以自定义实现configurer,比如:new BuilderStateMachineConfigurerAdapter<>();
        configBuilder.apply(this);

        StateMachineConfig<SpuStatesEnum, SpuEventsEnum> stateMachineConfig = configBuilder.getOrBuild();
        StateMachineModel<SpuStatesEnum, SpuEventsEnum> machineModel = getMachineModel(stateMachineConfig);
        StateMachineModelFactory<SpuStatesEnum, SpuEventsEnum> factory = stateMachineConfig.getModel().getFactory();

        return new ObjectStateMachineFactory<>(machineModel, factory);
    }

    private static StateMachineModel<SpuStatesEnum, SpuEventsEnum> getMachineModel(StateMachineConfig<SpuStatesEnum, SpuEventsEnum> stateMachineConfig) {
        StatesData<SpuStatesEnum, SpuEventsEnum> stateMachineStates = stateMachineConfig.getStates();
        TransitionsData<SpuStatesEnum, SpuEventsEnum> stateMachineTransitions = stateMachineConfig.getTransitions();
        ConfigurationData<SpuStatesEnum, SpuEventsEnum> stateMachineConfigurationConfig = stateMachineConfig.getStateMachineConfigurationConfig();
        // 设置StateMachineModel
        return new DefaultStateMachineModel<>(stateMachineConfigurationConfig, stateMachineStates, stateMachineTransitions);
    }

}

主要执行的核心配置如下,包括配置状态机,添加所有支持的状态,添加状态的变迁。

scss 复制代码
private void configure(StateMachineBuilder.Builder<SpuStatesEnum, SpuEventsEnum> builder, String machineId) {
    try {
        // 设置状态机id
        configMachineId(builder.configureConfiguration(), machineId);

        // 添加状态
        configureStates(builder.configureStates());

        // 添加状态变迁
        configureTransitions(builder.configureTransitions());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private void configMachineId(StateMachineConfigurationConfigurer<SpuStatesEnum, SpuEventsEnum> config, String machineId) throws Exception {
    config.withConfiguration().machineId(machineId);
}

private void configureStates(StateMachineStateConfigurer<SpuStatesEnum, SpuEventsEnum> config) throws Exception {
    config.withStates()
            .initial(SpuStatesEnum.NONE)
            .states(EnumSet.allOf(SpuStatesEnum.class));
}

private void configureTransitions(StateMachineTransitionConfigurer<SpuStatesEnum, SpuEventsEnum> transitions) throws Exception {
    //====创建草稿====
    transitions
            .withExternal()
            // 初始状态
            .source(SpuStatesEnum.INIT)
            //目标状态
            .target(SpuStatesEnum.DRAFT)
            // 事件
            .event(SpuEventsEnum.CREATE_DRAFT)
            // 过滤条件
            .guard(defaultSpuGuard)
            // 动作
            .action(spuCreateDraftSuccessAction, defaultSpuErrorAction)

            //====创建SPU====
            .and().withExternal()
            .source(SpuStatesEnum.INIT)
            .target(SpuStatesEnum.NEW)
            .event(SpuEventsEnum.CREATE_SPU)
            .guard(defaultSpuGuard)
            .action(defaultSpuSuccessAction, defaultSpuErrorAction)

            //====创建SPU (基于草稿创建spu )====
            .and().withExternal()
            .source(SpuStatesEnum.DRAFT)
            .target(SpuStatesEnum.NEW)
            .event(SpuEventsEnum.CREATE_SPU)
            .guard(defaultSpuGuard)
            .action(defaultSpuSuccessAction, defaultSpuErrorAction)

            //====提交审核====
            .and().withExternal()
            .source(SpuStatesEnum.NEW)
            .target(SpuStatesEnum.PENDING_REVIEW)
            .event(SpuEventsEnum.INITIATE_AUDIT)
            .guard(defaultSpuGuard)
            .action(defaultSpuSuccessAction, defaultSpuErrorAction)

            //====审核通过====
            .and().withExternal()
            .source(SpuStatesEnum.PENDING_REVIEW)
            .target(SpuStatesEnum.CM_APPROVED_PASS)
            .event(SpuEventsEnum.REVIEW_PASS)
            .guard(defaultSpuGuard)
            .action(defaultSpuSuccessAction, defaultSpuErrorAction)

            //====审核失败====
            .and().withExternal()
            .source(SpuStatesEnum.PENDING_REVIEW)
            .target(SpuStatesEnum.CM_APPROVED_REJECTION)
            .event(SpuEventsEnum.REVIEW_REJECTION)
            .guard(defaultSpuGuard)
            .action(defaultSpuSuccessAction, defaultSpuErrorAction)

            // 删除草稿
            .and().withExternal()
            .source(SpuStatesEnum.DRAFT)
            .target(SpuStatesEnum.CANCEL)
            .event(SpuEventsEnum.CANCEL)
            .guard(defaultSpuGuard)
            .action(spuCancelDraftSuccessAction, defaultSpuErrorAction)

            // 删除SPU
            .and().withExternal()
            .source(SpuStatesEnum.NEW)
            .target(SpuStatesEnum.CANCEL)
            .event(SpuEventsEnum.CANCEL)
            .guard(defaultSpuGuard)
            .action(spuCancelDraftSuccessAction, defaultSpuErrorAction)
    ;
}

然后是在 SpuStateMachineService 中封装状态机的调用入口,并且在 SpuStateMachineService 中会启动 Spring 容器。

php 复制代码
/**
 * 状态机  核心处理 类
 */
public class SpuStateMachineService {

    private final ApplicationContext applicationContext;
    private final StateMachineFactory<SpuStatesEnum, SpuEventsEnum> spuStateMachineFactory;
    private final StateMachinePersister<SpuStatesEnum, SpuEventsEnum, SpuMessageContext> spuStateMachinePersister;

    public SpuStateMachineService(String machineId) {
        // 启动Spring容器,获取 ApplicationContext 对象
        applicationContext = new AnnotationConfigApplicationContext(SpuStateMachineConfig.class);

        spuStateMachineFactory = applicationContext.getBean(StateMachineFactory.class);
        spuStateMachinePersister = applicationContext.getBean(StateMachinePersister.class);
    }

    /**
     * 发送事件
     *
     * @param event
     * @param context
     * @return
     */
    public boolean sendEvent(SpuEventsEnum event, SpuMessageContext context) {
        // 利用随记ID创建状态机,创建时没有与具体定义状态机绑定
        StateMachine<SpuStatesEnum, SpuEventsEnum> stateMachine = spuStateMachineFactory.getStateMachine(SpuStateMachineConfig.DEFAULT_MACHINEID);
        try {
            // restore
            spuStateMachinePersister.restore(stateMachine, context);
            // 构建 mesage
            Message<SpuEventsEnum> message = MessageBuilder.withPayload(event)
                    .setHeader("request", context)
                    .build();
            // 发送事件,返回是否执行成功
            boolean success = stateMachine.sendEvent(message);
            if (success) {
                spuStateMachinePersister.persist(stateMachine, context);
            }
            return success;
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("状态机处理未执行成功", e);
        } finally {
            stateMachine.stop();
        }
    }
}

Cola StateMachine

将主要的代码也都封装到两个类中:ColaStateMachineConfig 和 ColaStateMachineService。

首先是 ColaStateMachineConfig,主要负责 Cola StateMachine 的配置:

scss 复制代码
public class ColaStateMachineConfig<Context> {

    public StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> createBuilder() {
        StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> builder = StateMachineBuilderFactory.create();
        // 创建草稿
        builder.externalTransition()
                .from(SpuStateEnum.INIT)
                .to(SpuStateEnum.DRAFT)
                .on(SpuEventEnum.CREATE_DRAFT)
                .when(SpuEventEnum.CREATE_DRAFT.getCondition())
                .perform(SpuEventEnum.CREATE_DRAFT.getAction());
        // 创建SPU
        builder.externalTransition()
                .from(SpuStateEnum.INIT)
                .to(SpuStateEnum.NEW)
                .on(SpuEventEnum.CREATE_SPU)
                .when(SpuEventEnum.CREATE_SPU.getCondition())
                .perform(SpuEventEnum.CREATE_SPU.getAction());

        // 创建SPU(基于草稿)
        builder.externalTransition()
                .from(SpuStateEnum.DRAFT)
                .to(SpuStateEnum.NEW)
                .on(SpuEventEnum.CREATE_SPU)
                .when(SpuEventEnum.CREATE_SPU.getCondition())
                .perform(SpuEventEnum.CREATE_SPU.getAction());
        // 提交审核
        builder.externalTransition()
                .from(SpuStateEnum.NEW)
                .to(SpuStateEnum.PENDING_REVIEW)
                .on(SpuEventEnum.INITIATE_AUDIT)
                .when(SpuEventEnum.INITIATE_AUDIT.getCondition())
                .perform(SpuEventEnum.INITIATE_AUDIT.getAction());
        // 审核通过
        builder.externalTransition()
                .from(SpuStateEnum.PENDING_REVIEW)
                .to(SpuStateEnum.CM_APPROVED_PASS)
                .on(SpuEventEnum.REVIEW_PASS)
                .when(SpuEventEnum.REVIEW_PASS.getCondition())
                .perform(SpuEventEnum.REVIEW_PASS.getAction());
        // 审核拒绝
        builder.externalTransition()
                .from(SpuStateEnum.PENDING_REVIEW)
                .to(SpuStateEnum.CM_APPROVED_REJECTION)
                .on(SpuEventEnum.REVIEW_REJECTION)
                .when(SpuEventEnum.REVIEW_REJECTION.getCondition())
                .perform(SpuEventEnum.REVIEW_REJECTION.getAction());
        // 删除SPU
        builder.externalTransition()
                .from(SpuStateEnum.DRAFT)
                .to(SpuStateEnum.CANCEL)
                .on(SpuEventEnum.CANCEL)
                .when(SpuEventEnum.CANCEL.getCondition())
                .perform(SpuEventEnum.CANCEL.getAction());

        return builder;
    }
}

然后是 ColaStateMachineService,主要是封装了状态机的调用入口:

arduino 复制代码
public
class ColaStateMachineService<Context> 
{

    private final ColaStateMachineConfig<Context> config;
    private final StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> stateMachineBuilder;
    private final StateMachine<SpuStateEnum, SpuEventEnum, Context> stateMachine;

    public ColaStateMachineService(String machineId) {
        config = new ColaStateMachineConfig<>();
        stateMachineBuilder = config.createBuilder();
        stateMachine = stateMachineBuilder.build(machineId);
    }

    public StateMachineBuilder<SpuStateEnum, SpuEventEnum, Context> getStateMachineBuilder() {
        return stateMachineBuilder;
    }

    public StateMachine<SpuStateEnum, SpuEventEnum, Context> getStateMachine() {
        return stateMachine;
    }

    public SpuStateEnum sendEvent(SpuStateEnum source, SpuEventEnum event, Context context) {
        return getStateMachine().fireEvent(source, event, context);
    }

}

准备基准测试代码

基准测试是从吞吐量的维度做评测,预热 2 轮,使用 2 个进程,每个进程中有 8 个线程进行测试。

Spring StateMachine

less 复制代码
/**
 * 基准测试
 *
 * @auther houyi.wh
 * @date 2023-10-18 14:10:18
 * @since 0.0.1
 */
@Warmup(iterations = 2)
@BenchmarkMode({Mode.Throughput})
@Measurement(iterations = 2, time = 1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2)
@Threads(8)
@State(Scope.Benchmark)
public class SpringStateMachineBench {

    private SpuStateMachineService stateMachineService;

    @Setup
    public void prepare() {
        stateMachineService = new SpuStateMachineService("commodity-machine");
    }

    @Benchmark
    public void test_sendEvent() {
        SpuMessageContext entity = new SpuMessageContext("122312", "spu-1222", "https://111.baae.com/1241241.mp4");
        // 创建SPU,从INIT --> CREATE_SPU,如果符合条件则会执行doAction,并返回CREATE_SPU的状态,否则返回INIT
        boolean isSuccess = stateMachineService.sendEvent(SpuEventsEnum.CREATE_SPU, entity);
    }

    /**
     * 执行基准测试
     *
     * @param args
     * @throws RunnerException
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(SpringStateMachineBench.class.getSimpleName())
                .output("/Users/admin/Downloads/benchmark/spring-state-machine-benchmark.txt")
                .build();

        new Runner(opt).run();
    }

}

Cola StateMachine

less 复制代码
/**
 * 基准测试
 *
 * @auther houyi.wh
 * @date 2023-10-18 14:10:18
 * @since 0.0.1
 */
@Warmup(iterations = 2)
@BenchmarkMode({Mode.Throughput})
@Measurement(iterations = 2, time = 1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2)
@Threads(8)
@State(Scope.Benchmark)
public class ColaStateMachineBench {

    private ColaStateMachineService<SpuEntity> stateMachineService;

    @Setup
    public void prepare() {
        stateMachineService = new ColaStateMachineService<>("commodity-machine");
    }

    @Benchmark
    public void test_sendEvent() {
        SpuEntity entity = new SpuEntity("122312", "spu-1222", "https://111.baae.com/1241241.mp4");
        // 创建SPU,从INIT --> CREATE_SPU,如果符合条件则会执行doAction,并返回CREATE_SPU的状态,否则返回INIT
        SpuStateEnum spuStateEnum = stateMachineService.sendEvent(SpuStateEnum.INIT, SpuEventEnum.CREATE_SPU, entity);
    }

    /**
     * 执行基准测试
     *
     * @param args
     * @throws RunnerException
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(ColaStateMachineBench.class.getSimpleName())
                .output("/Users/admin/Downloads/benchmark/cola-state-machine-benchmark.txt")
                .build();

        new Runner(opt).run();
    }

}

测试结果汇总

测试结果对比如下,单位是每毫秒执行的次数,可以看到 Cola 是 Spring 的 1449 倍。

PS:由于这里测试的是框架本身的性能,doAction 中都是空实现,如果 doAction 使用实际的业务场景,根据木桶原理,最低的木板将决定木桶水位的高低,所以当 doAction 中有实际的 IO 操作时,两个框架的性能将会被 IO 操作的 RT 所拉齐。

具体的测试结果如下:

Spring StateMachine

shell 复制代码
JMH version: 1.35
# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=49634:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.shizhuang.duapp.statemachine.benchmark.SpringStateMachineBench.test_sendEvent

# Run progress: 0.00% complete, ETA 00:00:44
# Fork: 1 of 2
# Warmup Iteration   1: 17.855 ops/ms
# Warmup Iteration   2: 39.979 ops/ms
Iteration   1: 32.060 ops/ms
Iteration   2: 31.712 ops/ms

# Run progress: 50.00% complete, ETA 00:00:29
# Fork: 2 of 2
# Warmup Iteration   1: 16.947 ops/ms
# Warmup Iteration   2: 41.405 ops/ms
Iteration   1: 38.253 ops/ms
Iteration   2: 40.171 ops/ms


Result "com.shizhuang.duapp.statemachine.benchmark.SpringStateMachineBench.test_sendEvent":
  35.549 ±(99.9%) 27.813 ops/ms [Average]
  (min, avg, max) = (31.712, 35.549, 40.171), stdev = 4.304
  CI (99.9%): [7.736, 63.362] (assumes normal distribution)


# Run complete. Total time: 00:00:58

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                Mode  Cnt   Score    Error   Units
SpringStateMachineBench.test_sendEvent  thrpt    4  35.549 ± 27.813  ops/ms

六、基本功能评测

持久化一方面是好事,另一方面是坏事。

好事是因为持久化策略可以应对分布式系统的故障,每个实体对象在做状态变迁之前,可以从持久化的存储中获取该实体之前的状态,不用担心状态的丢失。

坏事是因为为了要保证状态机的状态,每次状态变迁之前都需要先恢复当前的状态,这个操作是非常消耗性能的。

Cola StateMachine 将 StateMachine 的实例定义为无状态(Stateless)的,状态的变迁不依赖当前 StateMachine 实例的状态,所以也就不需要持久化的问题。系统发生故障时,StateMachine 的实例也不需要重建,只需要对状态变迁做重试即可,状态是否能够变迁是在 Condition 中定义的,跟 StateMachine 的实例没有直接的关系。

七、接入成本评测

八、扩展能力评测

九、状态机对比总结

下面是一份详细的 Spring StateMachine、Squirrel StateMachine、Cola StateMachine 对比:

综合来看,三个状态机框架都有自己的优势和适用场景。Spring StateMachine 更适合应用于复杂业务场景,适合 Spring 生态中的应用;Squirrel StateMachine 更适合快速建模、轻量级场景;Cola StateMachine 更偏向于分布式系统和领域驱动设计。根据实际需求和项目特点选择适合的状态机框架更为重要。

十、商品域对于状态机的诉求

商品域对于状态机的诉求主要是希望:

  • 能够更清晰、更合理的管理和维护商品的状态。
  • 保证状态的变迁是符合业务场景的,不能产生错误的状态变迁。
  • 解决商品状态流转的过程管控的问题,将复杂的状态流转从耦合的业务中提取出来,让状态变迁的逻辑单独实现,便于后续状态的维护和扩展。

商品域对于状态机有以下这些使用场景:

  • 商品状态的状态值较多,状态之间的变迁较混乱,需要将散落在各个代码里维护不清晰的状态变迁统一维护。
  • SPU 的有多个表示状态的字段,需要在一个场景中同时维护多个状态字段的状态变迁。

十一、状态机选型总结

根据各状态机的功能、性能、接入成本、扩展能力,并结合商品领域对于状态机的使用诉求,主要是希望:

  • 能够更清晰、更合理的管理和维护商品的状态。
  • 保证状态的变迁是符合业务场景的,不能产生错误的状态变迁。
  • 解决商品状态流转的过程管控的问题,将复杂的状态流转从耦合的业务中提取出来,让状态变迁的逻辑单独实现,便于后续状态的维护和扩展。

另外状态机的性能不是特别关注的点,对于复杂的业务场景的支持是特别关注的点,综合来看,商品领域最终决定选择 Spring StateMachine 作为状态机的框架。

参考资料

www.uml-diagrams.org/state-machi... blog.csdn.net/significant... segmentfault.com/a/119000000... www.jianshu.com/u/c323ec8e0...

*文/逅弈

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

相关推荐
孤蓬&听雨2 天前
Kafka自动生产消息软件(自动化测试Kafka)
分布式·kafka·自动化·测试·生产者
帅得不敢出门5 天前
Python+Appium+Pytest+Allure自动化测试框架-安装篇
python·appium·自动化·pytest·测试·allure
陈明勇7 天前
自动化测试在 Go 开源库中的应用与实践
后端·go·测试
帅得不敢出门7 天前
Python+Appium+Pytest+Allure自动化测试框架-代码篇
python·appium·自动化·pytest·测试·allure
Dylanioucn8 天前
《解锁 TDD 魔法:高效软件开发的利器》
后端·功能测试·测试·测试驱动开发·tdd
北京_宏哥8 天前
《最新出炉》系列入门篇-Python+Playwright自动化测试-41-录制视频
前端·python·测试
努力的小雨9 天前
新手入门Java自动化测试的利器:Selenium WebDriver
后端·测试
画江湖Test14 天前
pytest 单元框架里,前置条件
python·自动化·pytest·测试·1024程序员节
城下秋草20 天前
AI测试之 TestGPT
测试工具·单元测试·ai编程·测试
Pandaconda20 天前
【新人系列】Python 入门(二):Python IDE 介绍
开发语言·ide·后端·python·面试·职场和发展·测试