一起养成写作习惯!这是我参与「掘金日新计划 · 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:状态转移之后执行的业务逻辑