以电商下单场景为例,玩一玩有限状态机的流转

1,752 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情

淘宝下单场景

现代数字社会给人们的生活带来了极大的便利,只需要在手机上点一点就可以购买到自己想要的商品,说到淘宝,用户就必然离不开下单这个功能。


以一个简化版的下单场景为例,一个订单在一开始创建之初是没有任何状态的,当用户点击下单按钮之后订单状态就会变成待付款,用户支付完成之后订单的状态会变成待收货状态,快递签收之后订单完成,如下图所示:

在这里插入图片描述 上面的订单流转图序是一个固定的流程,通常被称为有限状态机,也就是本文要分享给各位的主角。

实际上,在淘宝里,订单的状态非常多,不像上图中那样简单


有限状态机状态间流转要素

要实现状态与状态之间的流转,必须要具备以下几点要素,如下图所示:

在这里插入图片描述 1. 当前状态

状态流转的起始状态,如上图中的新建状态

2. 触发事件

引发状态与状态之间流转的事件,如上图中的创建订单这个动作

3. 响应函数

触发事件到下一个状态之间的规则

4. 目标状态

状态流转的终止状态,如上图中的待付款状态


状态机实现方式盘点

实现有限状态机可以通过以下我整理的其他方式,随着技术的发展,一定还有很多其他的思路,欢迎评论区补充~


1. MQ驱动

订单状态的流转可以通过MQ发布一个事件,消费者根据业务条件把订单状态进行流转,可以根据不同的事件发送到不同的Topic,使得业务逻辑更清晰

2. Job驱动

每隔一段时间启动一下job,根据特定的状态从数据库中拿对应的订单记录,然后判断订单是否有条件到达下一个状态

3. 轻量级状态机

Spring里也有这样的组件,Spring statemachine

上面三种方式非常轻量级,落地容易,如果不涉及非常多的状态流转或复杂的判断条件,就推荐使用这些方式

4. 规则引擎

这种方式比较重量级,通常是大厂才使用的玩法。业务团队可以在规则引擎里自行配置规则,不需要技术团队做发布和代码变更


基于Spring statemachine的轻量级状态机实现

经过上面对状态机的简单介绍,大家应该能建立起一个初始的印象,接下来就用一个实际案例演示一下如何编码吧~~


1. 创建maven项目,pom:

在pom文件里引入状态机statemachine,SpringBoot的启动类spring-boot-starter,测试类spring-boot-starter-test以及开发中常用的工具lombok

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>spring-statemachine-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

</project>

2. 创建订单状态枚举类:

该枚举类用于表示上面状态机流转图里的每一个订单状态,很简单

public enum OrderState {
    /*
    创建订单
     */
    CREATED,

    /*
    等待付款
     */
    PENDING_PAYMENT,

    /*
    等待配送
     */
    PENDING_DELIVERY,

    /*
    订单完结
     */
    ORDER_COMPLETE
}

3. 创建事件枚举类:

该枚举类用于表示上面状态机流转图里的每一个触发流转的动作,很简单

public enum OrderEvent {
    /*
    下单
     */
    PLACE_ORDER,
    
    /*
    付款完成
     */
    PAID,
    
    /*
    配送成功
     */
    DELIVERED
}

4. 创建事件监听器:

  • 使用@WithStateMachine注解开启状态机功能
  • @OnTransition(target = "PENDING_PAYMENT")注解的方法表示该操作会使状态流转至PENDING_PAYMENT
  • 定义pendingPayment方法表示用户创建订单操作
  • 定义pendingDelivery方法表示用户付款操作
  • 定义complete方法表示用户签收操作,订单状态变为完结
@WithStateMachine
@Slf4j
@Data
public class OrderListener {

    private String orderStatus = OrderState.CREATED.name();

    /**
     * 注解中传入的是目标状态
     *
     * @param message 可以从message中拿出一些属性
     */
    @OnTransition(target = "PENDING_PAYMENT")
    public void pendingPayment(Message message){
        log.info("订单创建,等待付款, status={} header={}", OrderState.PENDING_PAYMENT.name(),
                message.getHeaders().get("orderId"));

        // TODO 模拟业务流程
        setOrderStatus(OrderState.PENDING_PAYMENT.name());
    }

    @OnTransition(target = "PENDING_DELIVERY")
    public void pendingDelivery() {
        log.info("订单已付款,等待发货, status={} ",
                OrderState.PENDING_DELIVERY.name());
        // TODO 模拟业务流程
        setOrderStatus(OrderState.PENDING_DELIVERY.name());
    }

    @OnTransition(target = "ORDER_COMPLETE")
    public void complete() {
        log.info("订单完成, status={}",
                OrderState.ORDER_COMPLETE.name());
        // TODO 模拟业务流程
        setOrderStatus(OrderState.ORDER_COMPLETE.name());
    }
}

5. 创建配置类: 定义状态机的初始状态和状态流转的规则:

  • @EnableStateMachine表示开启状态机
  • 该类继承自EnumStateMachineConfigurerAdapter<OrderState, OrderEvent>并重写configure方法指定状态机的初始状态和状态机一共有哪些状态,以及状态之间是如何流转的
@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {


    @Override
    public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
        states.withStates()
                .initial(OrderState.CREATED)    // 指定初始化状态
                .states(EnumSet.allOf(OrderState.class));
    }

    /**
     * 配置状态机如何做流转
     *
     * @param transitions
     * @throws Exception
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
        transitions
                .withExternal()     // 配置两个不同状态之间的流转
                    .source(OrderState.CREATED)     // 创建订单
                    .target(OrderState.PENDING_PAYMENT)     // 待付款
                    .event(OrderEvent.PLACE_ORDER)  // 下单
                .and().withExternal()
                    .source(OrderState.PENDING_PAYMENT)
                    .target(OrderState.PENDING_DELIVERY)
                    .event(OrderEvent.PAID)
                .and().withExternal()
                    .source(OrderState.PENDING_DELIVERY)
                    .target(OrderState.ORDER_COMPLETE)
                    .event(OrderEvent.DELIVERED);
    }
}

6. 创建模拟发送一些事件的类:

手动触发一些事件

public class MyRunner implements CommandLineRunner {

    @Resource
    StateMachine<OrderState, OrderEvent> stateMachine;

    @Override
    public void run(String... args) throws Exception {
        stateMachine.start();

        Message message = MessageBuilder
                .withPayload(OrderEvent.PLACE_ORDER)    // 下单事件
                .setHeader("orderId", "998")    // 消息头里包含订单id
                .build();

        // 发送几个事件
        stateMachine.sendEvent(message);
        stateMachine.sendEvent(OrderEvent.PAID);    // 已付款
        stateMachine.sendEvent(OrderEvent.DELIVERED);   // 已发货
    }
}

7. 创建启动类:

SpringBoot标准的启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class ApplicationStarter {

    @Bean
    public MyRunner chickenRun() {
        return new MyRunner();
    }

    public static void main(String[] arg) {
        SpringApplication.run(ApplicationStarter.class, arg);
    }

}

8. 创建测试类把SpringBoot给跑起来:

@SpringBootTest
public class StateMachineTest {

    @Test
    public void runForrest() {

    }
}

运行结果:

MyRunner将会发送三个事件,这三个事件会触发Listener里三次订单状态的转换 请添加图片描述

此外,Spring statemachine还有一些高级功能:

  • Guard:状态准入、判断当前业务是否可以进入下个状态
  • Action:状态转移之后执行的业务逻辑