得物商品状态体系介绍

news2024/11/20 1:22:46

一、得物的商品体系

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

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

12.png

新品

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

13.png

商品

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

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

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

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

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

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

1210.png

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

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

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

1230.png

草稿

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

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

1090.png

二、得物商品状态流转

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

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

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

0120.png

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

三、状态机图

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

核心组件

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

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

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

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

890.png

状态类型

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

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

780.png

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

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

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

1200.png

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

四、状态机选型介绍

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

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

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

Enum StateMachine

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

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

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

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

public interface StateAction {
    void doAction();
}

状态机的枚举定义如下:

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:用于实现当前节点对应的业务逻辑处理

以下是一些核心组件:

765.png

Spring StateMachine 的核心实现:

786.png

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

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 状态机的配置,可以参考这篇文章:https://www.jianshu.com/p/b0c9e4f9d769

Squirrel StateMachine

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

核心组件:

090.png

Squirrel StateMachine 的核心实现:

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

0900.png

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

Cola StateMachine

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

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

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

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

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

1204.png

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

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

具体举例如下:

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

更详细的介绍 Cola StateMachine 的资料,可以参考作者的介绍:https://blog.csdn.net/significantfrank/article/details/104996419

五、状态机性能评测

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

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

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

准备测试代码

Spring StateMachine

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

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

/**
 * 状态机 核心配置
 */
@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);
    }

}

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

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 容器。

/**
 * 状态机  核心处理 类
 */
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 的配置:

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,主要是封装了状态机的调用入口:

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

/**
 * 基准测试
 *
 * @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

/**
 * 基准测试
 *
 * @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 所拉齐。

8001.png

具体的测试结果如下:

Spring StateMachine

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

六、基本功能评测

3405.png

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

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

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

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

七、接入成本评测

0001.png

八、扩展能力评测

2000.png

九、状态机对比总结

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

1008.png

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

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

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

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

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

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

十一、状态机选型总结

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

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

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

参考资料

https://www.uml-diagrams.org/state-machine-diagrams.htmlhttps://blog.csdn.net/significantfrank/article/details/104996419 https://segmentfault.com/a/1190000009906317 https://www.jianshu.com/u/c323ec8e077b1021

*文/逅弈

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1358524.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT工具栏开始,退出

QT工具栏开始&#xff0c;退出 //初始化场景QMenuBar *bar menuBar();setMenuBar(bar);QMenu *startbar bar->addMenu("开始");QAction * quitAction startbar->addAction("退出");connect(quitAction , &QAction::triggered,[](){this->c…

【linux】线程同步+基于BlockingQueue的生产者消费者模型

线程同步基于BlockingQueue的生产者消费者模型 1.线程同步2.生产者消费者模型3.基于BlockingQueue的生产者消费者模型 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&#xff01; 1.线程同步 在线程互斥写了一份抢票的代码&#xff0c;我们发现虽然加锁解决了抢到负数票的…

面试题:Spring Boot 中如何统计代码执行耗时

文章目录 ① StopWatch② System.nanoTime()③ new Date()④ System.currentTimeMillis() 开始 System.currentTimeMillis() 减去 结束 System.currentTimeMillis() 等于 耗时 其实我个人感觉OK的&#xff0c;就这样就蛮好的&#xff0c;很多项目都是这样用的。 简简单单的挺…

基于Java SSM框架实现游戏论坛平台系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现游戏论坛平台系统演示 摘要 本论文主要论述了如何使用java语言开发一个游戏论坛平台的设计&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构、ssm 框架和 java 开发的 Web 框架&#xff0c;基于Werkzeug WSGI工具箱和…

2024.1.5每日一题

LeetCode每日一题 1944.队列中可以看到的人数 1944. 队列中可以看到的人数 - 力扣&#xff08;LeetCode&#xff09; 题目描述 有 n 个人排成一个队列&#xff0c;从左到右 编号为 0 到 n - 1 。给你以一个整数数组 heights &#xff0c;每个整数 互不相同&#xff0c;heig…

将 validator 校验器从 ParameterValidator 中抽离出来

目录 一、前置说明1、总体目录2、相关回顾3、本节目标 二、操作步骤1、项目目录2、代码实现3、测试代码4、日志输出 三、后置说明1、要点小结2、下节准备 一、前置说明 1、总体目录 《 pyparamvalidate 参数校验器&#xff0c;从编码到发布全过程》 2、相关回顾 pyparamval…

科研上新 | 第6期:优化LLM数学推理;深度学习建模基因表达调控;基于深度学习的近实时海洋碳汇估算

编者按&#xff1a;欢迎阅读“科研上新”栏目&#xff01;“科研上新”汇聚了微软亚洲研究院最新的创新成果与科研动态。在这里&#xff0c;你可以快速浏览研究院的亮点资讯&#xff0c;保持对前沿领域的敏锐嗅觉&#xff0c;同时也能找到先进实用的开源工具。 本期内容速览 …

【深度学习:SENet】信道注意力和挤压激励网络(SENet):图像识别的新突破

【深度学习&#xff1a;SENet】信道注意力和挤压激励网络&#xff08;SENet&#xff09;&#xff1a;图像识别的新突破 为什么有效如何实现工作原理应用案例 挤压和激励网络&#xff08;SENets&#xff09;为卷积神经网络&#xff08;CNN&#xff09;引入了一个新的构建模块&am…

centos 7.9安装RocketMQ4.6.1版本

1.先下载二进制文件 下载 | RocketMQ 2.下载后&#xff0c;进行解压 unzip rocketmq-all-4.6.1-bin-release.zip 3.修改JVM配置 进到/datadrive/rocketmq-all-4.6.1-bin-release/bin下编辑runserver.sh 与 runbroker.sh文件 根据个人虚拟机大小进行修改 vi runserver.sh J…

这个应该是全网最全的接口测试工具之postman

概念 接口测试是什么&#xff1f; 百度百科给出的解释是&#xff1a; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间…

下载知虾数据分析软件:优化店铺运营、提高转化率的利器

在如今竞争激烈的电商市场中&#xff0c;对销售数据、流量以及买家行为等关键指标的监控和分析至关重要。Shopee平台为卖家提供了一个内置的在线数据分析工具——“Shopee Analytics”&#xff08;知虾分析&#xff09;&#xff0c;让卖家能够轻松实现对店铺运营的优化、提高转…

40 个简单又有效的 Linux Shell 脚本示例

【收藏】Linux系统常用命令速查手册&#xff08;附PDF下载方式&#xff09;_linux命令大全详解pdf-CSDN博客 历史上&#xff0c;shell 一直是类 Unix 系统的本地命令行解释器。它已被证明是 Unix 的主要功能之一&#xff0c;并发展成为一个全新的主题。Linux 提供了各种功能强大…

LeetCode 20.有效括号 详解(c语言实现) (⌯꒪꒫꒪)੭

题目详情&#xff1a; 思路&#xff1a;Step1:如果是左括号&#xff0c;入栈 Step2:如果是右括号&#xff0c;就出栈顶的元素与右括号进行比对&#xff0c;如果匹配&#xff0c;继续&#xff0c;直到都匹配成功结束。否则退出&#xff0c;直接返回false. 栗子&#xff1a;1. {…

C#之反编译之路(一)

本文将介绍微软反编译神器dnSpy的使用方法 c#反编译之路(一) dnSpy.exe区分64位和32位,所以32位的程序,就用32位的反编译工具打开,64位的程序,就用64位的反编译工具打开(个人觉得32位的程序偏多,如果不知道是32位还是64位,就先用32位的打开试试) 目前只接触到wpf和winform的桌…

Redis (三)

1、redis复制 简单的概括就是主从复制&#xff0c;master以写为主&#xff0c;Slave以读为主&#xff0c;当master数据发生变化的时候&#xff0c;自动将更新的数据异步同步到其他的slave是数据库。 使用这种机制的话&#xff0c;可以做到读写分离&#xff0c;可以减轻主机负担…

DDoS攻击的多种方式

DDOS攻击指分布式拒绝服务攻击&#xff0c;即处于不同位置的多个攻击者同时向一个或数个目标发动攻击&#xff0c;或者一个攻击者控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击。由于攻击的发出点是分布在不同地方的&#xff0c;这类攻击称为分布式拒绝服务…

大数据毕设分享 flink大数据淘宝用户行为数据实时分析与可视化

文章目录 0 前言1、环境准备1.1 flink 下载相关 jar 包1.2 生成 kafka 数据1.3 开发前的三个小 tip 2、flink-sql 客户端编写运行 sql2.1 创建 kafka 数据源表2.2 指标统计&#xff1a;每小时成交量2.2.1 创建 es 结果表&#xff0c; 存放每小时的成交量2.2.2 执行 sql &#x…

提升网络安全重要要素IP地址

在数字化时代&#xff0c;网络安全已经成为人们关注的焦点。本文将深入探讨网络安全与IP地址之间的紧密联系&#xff0c;以及IP地址在构建数字世界的前沿堡垒中的关键作用。 网络安全是当今数字社会中不可忽视的挑战之一。而IP地址&#xff0c;作为互联网通信的基础协议&#…

震惊!原来这就是JavaScript闭包的秘密

&#x1f4e2; 鸿蒙专栏&#xff1a;想学鸿蒙的&#xff0c;冲 &#x1f4e2; C语言专栏&#xff1a;想学C语言的&#xff0c;冲 &#x1f4e2; VUE专栏&#xff1a;想学VUE的&#xff0c;冲这里 &#x1f4e2; CSS专栏&#xff1a;想学CSS的&#xff0c;冲这里 &#x1f4…

第一届能源电子产业创新大赛太阳能光伏赛道决赛及颁奖仪式在宜宾成功举办

在工业和信息化部电子信息司指导下&#xff0c;由工业和信息化部产业发展促进中心和宜宾市人民政府主办&#xff0c;宜宾市经济和信息化局、宜宾高新技术产业园区管理委员会承办的第一届能源电子产业创新大赛太阳能光伏赛道决赛及颁奖仪式于2024年1月3日-5日在宜宾市成功举办。…