Spring-statemachine实现订单状态机

2023-10-17 08:39:46 浏览数 (2)

状态机简介

先从状态机的定义入手,StateMachine,其中:

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

状态机的相关概念

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

状态机的持久化

每次用到的时候新创建一个状态机,太奢侈了,官方文档里面也提到过这点。而且创建出来的实例,其状态也跟当前订单的不符;spring statemachine暂时不支持每次创建时指定当前状态,所以对状态机引擎实例的持久化,就成了必须要考虑的问题。

spring statemachine 本身支持了内存、redis及db的持久化,内存持久化就不说了,看源码实现就是放在了hashmap里,平时也没谁项目中可以这么奢侈,啥啥都放在内存中,而且一旦重启…(嘿嘿嘿)。下面详细说下利用redis进行的持久化操作。

spring statemachine持久化时,采用了三层结构设计,persister —>persist —>repository。

  • 其中persister中封装了write和restore两个方法,分别用于持久化写及反序列化读出。
  • persist只是一层皮,主要还是调用repository中的实际实现,这里可以使用
  • repository中做了两件事儿
    • 序列化/反序列化数据,将引擎实例与二进制数组互相转换
    • 读、写redis
Persister

接口

代码语言:javascript复制
public interface StateMachinePersister<S, E, T> {
    void persist(StateMachine<S, E> var1, T var2) throws Exception;

    StateMachine<S, E> restore(StateMachine<S, E> var1, T var2) throws Exception;
}

接口点进去其抽象类的实现,可以看到使用的是StateMachinePersist的read与write方法

代码语言:javascript复制
public abstract class AbstractStateMachinePersister<S, E, T> implements StateMachinePersister<S, E, T> {
    private final StateMachinePersist<S, E, T> stateMachinePersist;
    public final void persist(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
        this.stateMachinePersist.write(this.buildStateMachineContext(stateMachine), contextObj);
    }

    public final StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
        final StateMachineContext<S, E> context = this.stateMachinePersist.read(contextObj);
        stateMachine.stop();
        stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {
            public void apply(StateMachineAccess<S, E> function) {
                function.resetStateMachine(context);
            }
        });
        stateMachine.start();
        return stateMachine;
    }
}
Persist
代码语言:javascript复制
public interface StateMachinePersist<S, E, T> {
    void write(StateMachineContext<S, E> var1, T var2) throws Exception;

    StateMachineContext<S, E> read(T var1) throws Exception;
}

自定义实现

代码语言:javascript复制
@Component
public class OrderStateMachinePersist implements StateMachinePersist<OrderStatusEnum, OrderChangeEventEnum, PersisterDO> {

    @Autowired
    private TbUserOrderMapper userOrderMapper;

    //将订单状态写入数据库
    @Override
    public void write(StateMachineContext<OrderStatusEnum, OrderChangeEventEnum> stateMachineContext, PersisterDO persisterDO) throws Exception {
        String orderNumber = persisterDO.getOrderNumber();
        if(orderNumber == null){
            throw new RuntimeException("orderNumber 为空");
        }
        QueryWrapper<TbUserOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("order_number",orderNumber);
        TbUserOrder order = userOrderMapper.selectOne(wrapper);
        if(order != null){
            order.setOrderState(stateMachineContext.getState().name());
            order.setUpdateTime(new Date());
            userOrderMapper.updateById(order);
        }
    }

    @Override
    public StateMachineContext<OrderStatusEnum, OrderChangeEventEnum> read(PersisterDO persisterDO) throws Exception {
        //将数据从数据库中读出,然后将订单的状态设置给参数orderStatusEnum
        OrderStatusEnum orderStatusEnum;
        QueryWrapper<TbUserOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("order_number",persisterDO.getOrderNumber());
        TbUserOrder order = userOrderMapper.selectOne(wrapper);
        if(order != null){
            orderStatusEnum = OrderStatusEnum.valueOf(order.getOrderState());
        }else{
            orderStatusEnum = OrderStatusEnum.CREATE;
        }
        return new DefaultStateMachineContext<>(orderStatusEnum, null, null, null, null, persisterDO.getMachineId());
    }
}

状态变迁

状态枚举就是当前订单所处的状态,事件会导致订单的状态发生改变(但是也不一定,有些是内部事件,并不会导致状态发生变化)

状态枚举
代码语言:javascript复制
public enum OrderStatusEnum {
    CREATE("新建"),
    WAIT_PAYMENT("待付款"),
    WAIT_SEND("待发货"),
    WAIT_RECEIVE("待收货"),
    COMPLETED("已完成"),
    CANCEL("取消");

    private final String name;
}
事件枚举
代码语言:javascript复制
public enum OrderChangeEventEnum {
    CREATE_ORDER("创建订单"),
    PAY_ORDER("支付订单"),
    PAY_CANCEL("取消支付"),
    SEND_GOODS("发货"),
    RECEIVE_GOODS("收货"),
    ;
    private final String name;
}

状态机的配置

状态机的配置有两种方式

  1. 创建config类,实现StateMachineConfigurer(或者根据SE的不同,直接继承其子类StateMachineConfigurerAdapter、EnumStateMachineConfigurerAdapter),然后分别重写其不同的configure方法,用于指定对应配置。
  2. 依然实现StateMachineConfigurer(或继承其子类),不过通过StateMachineBuilder.builder()来指定对应配置。
自定义Builder

自定义一个Builder接口,用来规范不同业务状态机的配置

代码语言:javascript复制
public interface IStateMachineBuilder<S, E> {
    String getName();

    StateMachine<S, E> build(BeanFactory beanFactory) throws Exception;

    //订单状态机构造器
    String ORDER_BUILDER_NAME = "orderStateMachineBuilder";
}
设置对应业务Builder

设置对应业务的Builder,外部调用的使用直接使用build()方法就行

这个里面的Guard就可以相当于一个条件,如果不满足Guard(即返回false)那么就不会执行接下来的action

Action就相当于一个执行的过程,其中errorHandlerAction就是对异常的处理,其中比较重要的是

  • withExternal 是当source和target不同时的写法,比如付款成功后状态发生的变化。
  • withInternal 当source和target相同时的串联写法,比如付款失败后都是待付款状态。
  • withExternal的source和target用于执行前后状态、event为触发的事件、guard判断是否执行action。同时满足source、target、event、guard的条件后执行最后的action。
  • withChoice 当执行一个动作,可能导致多种结果时,可以选择使用choice guard来跳转
  • withChoice根据guard的判断结果执行first/then的逻辑。
  • withChoice不需要发送事件来进行触发。
代码语言:javascript复制
public class OrderStateMachineBuilder implements IStateMachineBuilder<OrderStatusEnum, OrderChangeEventEnum> {

    @Autowired
    private Guard<OrderStatusEnum, OrderChangeEventEnum> orderCreateGuard;

    @Autowired
    private Action<OrderStatusEnum, OrderChangeEventEnum> errorHandlerAction;

    @Autowired
    private Action<OrderStatusEnum, OrderChangeEventEnum> orderCreateAction;


    @Override
    public StateMachine<OrderStatusEnum, OrderChangeEventEnum> build(BeanFactory beanFactory) throws Exception {
        StateMachineBuilder.Builder<OrderStatusEnum, OrderChangeEventEnum> builder = StateMachineBuilder.builder();
        //设置id
        builder.configureConfiguration()
                .withConfiguration()
                .autoStartup(true)
                .beanFactory(beanFactory)
                .machineId(ORDER_BUILDER_NAME   "Id");

//初始化状态机,并指定状态集合
        builder.configureStates()
                .withStates()
                //初始状态
                .initial(OrderStatusEnum.CREATE)
                .end(OrderStatusEnum.COMPLETED)
                .states(EnumSet.allOf(OrderStatusEnum.class));

        //定义状态机节点,即迁移动作
        builder.configureTransitions()
                .withExternal()
                .source(OrderStatusEnum.CREATE)
                .target(OrderStatusEnum.WAIT_PAYMENT)
                .event(OrderChangeEventEnum.CREATE_ORDER)
                .guard(orderCreateGuard)
                .action(orderCreateAction, errorHandlerAction)
            
            ...这可以使用and()继续写
        ;

        return builder.build();
    }

    @Override
    public String getName() {
        return ORDER_BUILDER_NAME;
    }
}
状态机工厂

在实际项目中一般都会有多个状态机并发执行,比如订单,同一时刻会有不止一个订单在运行,而每个订单都有自己的订单状态机流程。所以如果使用配置类的话就只有一个状态机,所以需要使用Builder,同时因为可以会有多种类型的状态机,所以定义了一个接口,后续类型的状态机只要实现这个状态机接口就可以开发

代码语言:javascript复制
@Component
public class StateMachineBuildFactory<S, E> implements ApplicationContextAware {

    @Autowired
    private BeanFactory beanFactory;
    /**
     * 用来存储builder-name及builder的map
     */
    public static final Map<String, IStateMachineBuilder> builderMap = new ConcurrentHashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        String[] beanNamesForType = applicationContext.getBeanNamesForType(IStateMachineBuilder.class);
        for (String beanName : beanNamesForType) {
            IStateMachineBuilder bean = (IStateMachineBuilder) applicationContext.getBean(beanName);
            builderMap.put(bean.getName(), bean);
        }
    }

    public StateMachine<S, E> createStateMachine(String machineType) throws Exception{
        if (StringUtils.isBlank(org.apache.commons.lang3.StringUtils.trim(machineType))) {
            throw new RuntimeException("无效的状态机类型");
        }
        IStateMachineBuilder builder = builderMap.get(machineType);
        StateMachine<S, E> stateMachine;
        try {
            stateMachine = builder.build(beanFactory);
        } catch (Exception e) {
            throw new RuntimeException("创建状态机异常");
        }

        return stateMachine;
    }
}

状态机的使用

外部调用状态机需要以下三步

  1. 通过创建/读取的方式获取当前订单对应的状态机引擎实例
  2. 构造message
  3. 发送message

需要注意的是当发送完message之后,spring statemachine才会通过监听器来监听走那个action,只有在message完成之后才会更新为target状态

所以为了更加方便管理使用,所以做了以下代码封装

接口Manager

这规定了这个Manager必须实现的方法,就是来发送message

代码语言:javascript复制
public interface IStateMachineEventManager<E>{
    /**
     * 状态发生改变变更事件
     * @param statusModelDO
     * @param eventEnum
     * @param <R>
     * @return
     */
    <R> R sendStatusChangeEvent(StatusModelDO statusModelDO, E eventEnum);
}
抽象类

这个里面主要实现初始化状态机,构造message与发送message

代码语言:javascript复制
public abstract class AbstractStateMachineEventManager<S, E> implements IStateMachineEventManager<E> {
    @Autowired
    private StateMachineBuildFactory<S, E> stateMachineBuildFactory;

    //用于状态机上下文持久化
    public abstract void stateMachinePersist(StateMachine<S, E> stateMachine, PersisterDO persisterDO) throws Exception;

    //用于状态机上下文初始化
    public abstract StateMachine<S, E> stateMachineRestore(StateMachine<S, E> stateMachine, PersisterDO persisterDO) throws Exception;

    @Override
    @SuppressWarnings("unchecked")
    public <R> R sendStatusChangeEvent(StatusModelDO statusModelDO, E eventEnum) {
        StateMachine<S, E> stateMachine = initializeMachine(statusModelDO);
        Boolean result = statusChangeExecute(stateMachine, statusModelDO, eventEnum);
        if (!result) {
            throw new RuntimeException("状态机状态执行失败");
        }
        RuntimeException exception = (RuntimeException) stateMachine.getExtendedState().getVariables().get(RuntimeException.class);
        if (exception != null) {
        }
        try {
            PersisterDO persisterDO = new PersisterDO();
            persisterDO.setOrderNumber(statusModelDO.getOrderNumber());
            stateMachinePersist(stateMachine, persisterDO);
        } catch (Exception e) {
            throw new RuntimeException("状态机持久化失败");
        }

        return (R) stateMachine.getExtendedState().getVariables().get(StateMachineConstants.RETURN_PARAM);
    }

    private StateMachine<S, E> initializeMachine(StatusModelDO statusModelDO) {
        StateMachine<S, E> stateMachine;
        try {
            StateMachine<S, E> srcStateMachine = stateMachineBuildFactory.createStateMachine(getStateMachineType());
            PersisterDO persisterDO = new PersisterDO();
            persisterDO.setOrderNumber(statusModelDO.getOrderNumber());
            persisterDO.setCurrentState(statusModelDO.getCurrentState());
            stateMachine = stateMachineRestore(srcStateMachine, persisterDO);
        } catch (Exception e) {
            throw new RuntimeException("初始化状态机失败");
        }
        if (stateMachine == null) {
            throw new RuntimeException("没有找到可用的状态机");
        }
        return stateMachine;
    }

    private boolean statusChangeExecute(StateMachine<S, E> stateMachine, StatusModelDO statusModelDO, E eventEnum) {
        Message<E> eventMsg = MessageBuilder.withPayload(eventEnum)
                .setHeader(StateMachineConstants.STATE_MODEL_DTO, statusModelDO).build();
        if (!acceptEvent(stateMachine, eventMsg)) {
            throw new RuntimeException("找不到对应状态机事件触发定义");
        }

        return stateMachine.sendEvent(eventMsg);
    }
    private static <S, E> boolean acceptEvent(StateMachine<S, E> stateMachine, Message<E> eventMsg) {
        //获取当前状态
        State<S, E> currentState = stateMachine.getState();

        for (Transition<S, E> transition : stateMachine.getTransitions()) {
            State<S, E> source = transition.getSource();
            Trigger<S, E> trigger = transition.getTrigger();
            if (currentState != null && trigger != null &&
                    StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds()) &&
                    trigger.evaluate(new DefaultTriggerContext<>(eventMsg.getPayload()))) {
                return true;
            }
        }

        return false;
    }

    public abstract String getStateMachineType();
}
实现类

这里主要实现的是上下文的初始化与持久化,这个就可以直接调用状态机的持久化里面的read与write就行

代码语言:javascript复制
@Component
public class OrderStateMachineEventManager extends AbstractStateMachineEventManager<OrderStatusEnum, OrderChangeEventEnum> {
    @Resource(name = "orderStateMachinePersister")
    private StateMachinePersister<OrderStatusEnum, OrderChangeEventEnum, PersisterDO> stateMachinePersister;

    @Override
    public void stateMachinePersist(StateMachine<OrderStatusEnum, OrderChangeEventEnum> stateMachine,
                                    PersisterDO persisterDO) throws Exception {
        stateMachinePersister.persist(stateMachine, persisterDO);
    }

    @Override
    public StateMachine<OrderStatusEnum, OrderChangeEventEnum> stateMachineRestore(
            StateMachine<OrderStatusEnum, OrderChangeEventEnum> stateMachine,
            PersisterDO persisterDO) throws Exception {
        return stateMachinePersister.restore(stateMachine, persisterDO);
    }

    @Override
    public String getStateMachineType() {
        return IStateMachineBuilder.ORDER_BUILDER_NAME;
    }
}

参考文档

Github代码

状态机引擎选型

官网

reference

0 人点赞