什么是状态机

需求说明

在订单模块中订单共设计了7种状态,如下图

whiteboard_exported_image (8)

订单会在多个状态间进行转换,这就需要开发人员都去记忆这些转换关系,这是非常麻烦的

1
2
3
4
5
6
7
8
9
10
if(订单状态 == 待支付){
//如果用户支付成功,执行此场景下的业务逻辑,更新订单状态为派单中
update(id,派单中)
)

if(订单状态 == 待支付){
//如果用户取消支付,执行此场景下的业务逻辑。更新订单状态为已取消
update(id,已取消)
)
...

针对这种情况,可以引入状态机对订单状态进行统一管理,可以认为状态机就是一个封装好的组件

调用它的时候,只需要告诉它订单id和要执行的事件,它内部就可以完成对应的所有操作,类似于下面代码

1
2
3
4
5
//调用状态机执行支付成功事件
orderStateMachine.changeStatus(订单id,支付成功事件);

//调用状态机执行支付取消事件
orderStateMachine.changeStatus(订单id,支付取消事件);

因此使用状态机的好处就是:

  1. 易于理解:可以使业务模型清晰,开发人员可以更好地理解状态转换流程
  2. 方便调用:可以使调用者以不关注细节的角度去使用其暴露的接口方法

当然使用状态机也是有缺点的:

  1. 代码复杂:状态机需要较多接口和实现类,因此代码复杂度会高一点
  2. 运行效率:状态机需要经常创建状态机实例,运行效率会稍微差一点

状态机介绍

状态机是一种抽象的数学模型,用于描述事物在不同状态之间转移和行为变化的过程

它的核心是将状态之间的变更定义为事件,然后将事件暴露出来,通过执行状态变更事件去更改状态

理解状态机设计模式需要理解四个要素:现态、次态、事件、动作

  • 现态:是指当前所处的状态,比如说下图的待支付
  • 次态:条件满足后要转变为的新状态,比如说下图的派单中
  • 事件:状态变更的触发条件,比如说下图的用户支付成功事件
  • 动作:事件发生时执行的操作,将订单状态由待支付更改为派单中,它不是必需的

拿待支付状态到派单中状态举例

71305f93-4dc5-4a1d-8221-9d223446fba3

编写订单状态机

实现状态机步骤比较复杂,核心步骤如下:

  1. 引入通用状态机组件,内部定义了一些类和接口,订单状态机要去继承或实现这些类和接口
  2. 创建状态类,定义订单所有的状态,里面就包含所有现态和次态
  3. 创建事件类,定义触发订单状态改变的事件,它关联现态和次态
  4. 创建快照类,用来记录事件发生时,订单变化瞬间的状态及相关信息
  5. 创建动作类,用来执行订单状态发生改变需要触发的操作
  6. 创建状态机类,定义状态机的名称和初始状态

添加依赖

在jzo2o-orders-base工程的pom.xml中添加状态机组件的依赖

1
2
3
4
5
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-statemachine</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

创建订单状态类

定义订单所有的状态,实现接口StatusDefine

882dec8b-1c7b-43c2-b6be-ba111ebd02ba

创建订单事件类

定义触发订单状态改变的事件,它关联现态和次态,实现接口StatusChangeEvent

5570c1d3-ab7d-4840-a48f-65a304b46db5

创建订单快照类

用来记录事件发生时,订单变化瞬间的状态及相关信息,继承StateMachineSnapshot,例如:

  • 001号订单创建成功,此时会记录它的快照信息(订单号、下单人、订单详细信息、订单状态等)
  • 001号订单支付成功,此时也会记录它的快照信息

订单快照可以追溯订单的历史变化信息,只要状态发生变化便会记录快照

6806a570-7242-4292-a85c-efb63b0e36bc

创建订单动作类

用来执行订单状态发生改变需要触发的操作,实现StatusChangeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.jzo2o.orders.base.handler;

import com.jzo2o.orders.base.model.dto.OrderSnapshotDTO;
import com.jzo2o.orders.base.service.IOrdersCommonService;
import com.jzo2o.statemachine.core.StatusChangeEvent;
import com.jzo2o.statemachine.core.StatusChangeHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

//订单支付事件逻辑处理器
@Slf4j
@Component("order_payed")//接口实现类的bean名称规则为:状态机名称_事件名称
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {

@Autowired
private IOrdersCommonService ordersService;

/**
* 订单支付处理逻辑
*
* @param bizId 业务id
* @param bizSnapshot 快照
*/
@Override
public void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {
log.info("支付成功事件处理逻辑开始,订单号:{}", bizId);

}
}

创建订单状态机类

定义状态机的名称和初始状态,继承AbstractStateMachine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.jzo2o.orders.base.config;

import com.jzo2o.orders.base.enums.OrderStatusEnum;
import com.jzo2o.orders.base.model.dto.OrderSnapshotDTO;
import com.jzo2o.statemachine.AbstractStateMachine;
import com.jzo2o.statemachine.persist.StateMachinePersister;
import com.jzo2o.statemachine.snapshot.BizSnapshotService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
* 订单状态机
*/
@Component
public class OrderStateMachine extends AbstractStateMachine<OrderSnapshotDTO> {

public OrderStateMachine(StateMachinePersister stateMachinePersister, BizSnapshotService bizSnapshotService, RedisTemplate redisTemplate) {
super(stateMachinePersister, bizSnapshotService, redisTemplate);
}

/**
* 设置状态机名称
*
* @return 状态机名称
*/
@Override
protected String getName() {
return "order";
}

/**
* 设置状态机初始状态
*
* @return 状态机初始状态
*/
@Override
protected OrderStatusEnum getInitState() {
return OrderStatusEnum.NO_PAY;
}


/**
* 后置处理器 订单创建之后要做的操作,暂时啥也不做
*
* @param orderSnapshotDTO 订单快照
*/
@Override
protected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) {

}
}

AbstractStateMachine状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类

52e8bad1-65f6-40f5-971c-4e227690ec49

在整个状态机运转过程中需要两张数据表来记录数据,分别是

  • 状态机表:每个订单在此表中有一条数据,里面存储订单的最新状态
  • 状态机快照表:每个订单在此表有多条记录,里面存储的是订单到现在为止历经的状态

86f13bc4-b3e3-4cee-87e8-bca72637e58e

测试订单状态机

加载订单状态机

首先,在jzo2o-orders-base工程的AutoImportConfiguration类中配置导入订单状态机

0bd4d3b6-aeb2-422c-a62e-6ce61f00531d

测试启动状态机

在jzo2o-orders-manager下编写测试代码

调用OrderStateMachine的start()方法启动一个订单的状态机,它会设置订单的初始状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.jzo2o.orders.manager.service;

@SpringBootTest
@Slf4j
public class OrderStateMachineTest {

@Resource
private OrderStateMachine orderStateMachine;

@Test
public void test_start() {
//启动状态机,指定订单id,设置现态
String start = orderStateMachine.start("888");
log.info("返回初始状态:{}", start);
}
}

执行测试方法,对888订单启动状态机管理,启动后888号订单的状态为NO_PAY待支付状态

观察state_persister表有一条888号订单的状态持久化记录

205c0f7a-e6ca-4275-a7a8-817da18b9f91

观察biz_snapshot表有一条888号订单的快照信息,一条订单在biz_snapshot表对应多个条记录,每次订单状态变更都会产生一个快照

e948fc7f-64b9-4513-8f6e-7370c78ec348

注意:如果报错:java.lang.IllegalStateException: 已存在状态,不可初始化 ,说明888号订单的状态机已启动,可以更改101测试其它订单状态机启动

源码阅读

进入jzo2o-framework下的jzo2o-statemachine工程,阅读AbstractStateMachine类的源码

通过阅读代码理解状态机组件的运行过程

start-启动状态机

  1. 向状态机表保存一条记录
  2. 向状态机快照表保存一条记录

changeStatus-状态变更

  1. 从容器中根据 状态机名字_事件名称 找到一个对象, 调用对象的handler方法
  2. 更新状态机表中的状态字段
  3. 向状态机快照表保存一条记录

启动状态机

先判断该订单是否启动状态,如果没有启动则向状态机表插入记录,否则抛出异常”已存在状态,不可初始化”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 状态机初始化,不保存快照
* @param bizId 业务id
* @return 初始化状态代码
*/
public String start(String bizId) {
return start(null, bizId, initState, null);
}

/**
* 启动状态机,并设置当前状态和保存业务快照,快照分库分表
* @param dbShardId 分库键
* @param bizId 业务id
* @param statusDefine 当前状态
* @param bizSnapshot 快照
* @return 当前状态代码
*/
public String start(Long dbShardId, String bizId, StatusDefine statusDefine, T bizSnapshot) {

//1.初始化状态机状态
String currentState = stateMachinePersister.getCurrentState(name, bizId);
if (ObjectUtil.isEmpty(currentState)) {
stateMachinePersister.init(name, bizId, statusDefine);
} else {
throw new IllegalStateException("已存在状态,不可初始化");
}

//2.保存业务快照
if (bizSnapshot == null) {
bizSnapshot = ReflectUtil.newInstance(getSnapshotClass());
}
//设置快照id
bizSnapshot.setSnapshotId(bizId);
//设置快照状态
bizSnapshot.setSnapshotStatus(statusDefine.getStatus());
//快照转json
String bizSnapshotString = JSONUtil.toJsonStr(bizSnapshot);
if (ObjectUtil.isNotEmpty(bizSnapshot)) {
bizSnapshotService.save(dbShardId, name, bizId, statusDefine, bizSnapshotString);
}
//执行后处理方法
postProcessor(bizSnapshot);

return statusDefine.getCode();
}

状态变更

状态变更前会判断订单的当前状态是否和事件定义的源状态一致

如果不一致则说明当前订单的状态不能通过该事件去更新状态,此时将终止状态变更

否则将通过状态变更处理器更新订单的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 变更状态并保存快照,快照不进行分库
*
* @param bizId 业务id
* @param statusChangeEventEnum 状态变换事件
*/
public void changeStatus(String bizId, StatusChangeEvent statusChangeEventEnum) {
changeStatus(null, bizId, statusChangeEventEnum, null);
}

/**
* 变更状态并保存快照,快照分库分表
*
* @param dbShardId 分库键
* @param bizId 业务id
* @param statusChangeEventEnum 状态变换事件
* @param bizSnapshot 业务数据快照(json格式)
*/
public void changeStatus(Long dbShardId, String bizId, StatusChangeEvent statusChangeEventEnum, T bizSnapshot) {
//1.查询当前状态
String statusCode = getCurrentState(bizId);

//2.校验起止状态是否与事件匹配
if (ObjectUtil.isNotEmpty(statusChangeEventEnum.getSourceStatus()) && ObjectUtil.notEqual(statusChangeEventEnum.getSourceStatus().getCode(), statusCode)) {
throw new CommonException(HTTP_INTERNAL_ERROR, "状态机起止状态与事件不匹配");
}

//3.获取状态处理程序bean
//事件代码
String eventCode = statusChangeEventEnum.getCode();
StatusChangeHandler bean = null;
try {
bean = SpringUtil.getBean(name + "_" + eventCode, StatusChangeHandler.class);
} catch (Exception e) {
log.info("不存在‘{}’StatusChangeHandler", name + "_" + eventCode);
}
if (bizSnapshot == null) {
bizSnapshot = ReflectUtil.newInstance(getSnapshotClass());
}
//设置快照id
bizSnapshot.setSnapshotId(bizId);
//设置目标状态
bizSnapshot.setSnapshotStatus(statusChangeEventEnum.getTargetStatus().getStatus());
if (ObjectUtil.isNotNull(bean)) {
//4.执行状态变更
bean.handler(bizId, statusChangeEventEnum, bizSnapshot);
}

//5.状态持久化
stateMachinePersister.persist(name, bizId, statusChangeEventEnum.getTargetStatus());

//6、存储快照
if (ObjectUtil.isNotEmpty(bizSnapshot)) {
//构建新的快照信息
bizSnapshot = buildNewSnapshot(bizId, bizSnapshot, statusChangeEventEnum.getSourceStatus());
String newBizSnapShotString = JSONUtil.toJsonStr(bizSnapshot);
bizSnapshotService.save(dbShardId, name, bizId, statusChangeEventEnum.getTargetStatus(), newBizSnapShotString);
}

//7.清理快照缓存
String key = "JZ_STATE_MACHINE:" + name + ":" + bizId;
redisTemplate.delete(key);

//执行后处理方法
postProcessor(bizSnapshot);
}

项目集成

下单时启动状态机

下单完成后,使用状态机的启动方法开启状态机对该订单状态的管理

修改OrdersCreateServiceImpl类中保存订单的方法,添加启动状态机的逻辑

注意:状态机的操作方法要和业务方法处于一个事务中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Autowired
private OrderStateMachine orderStateMachine;

@Transactional
public void saveOrders(Orders orders) {
//保存到数据表
this.save(orders);

//构建快照
OrderSnapshotDTO orderSnapshotDTO = BeanUtil.toBean(this.getById(orders.getId()), OrderSnapshotDTO.class);

//启动状态机
orderStateMachine.start(null,orders.getId().toString(),orderSnapshotDTO);
}

@GlobalTransactional
public void saveOrdersWithCoupon(Orders orders, Long couponId) {
//1. 调用优惠券微服务核销优惠券
CouponUseReqDTO couponUseReqDTO = new CouponUseReqDTO();
couponUseReqDTO.setId(couponId);//优惠券id
couponUseReqDTO.setOrdersId(orders.getId());//订单id
couponUseReqDTO.setTotalAmount(orders.getTotalAmount());//总金额
CouponUseResDTO couponUseResDTO = couponApi.use(couponUseReqDTO);

//2. 修改订单的优惠金额和实付金额
BigDecimal discountAmount = couponUseResDTO.getDiscountAmount();
orders.setDiscountAmount(discountAmount);//优惠金额
orders.setRealPayAmount(orders.getTotalAmount().subtract(discountAmount));//实付金额

//3. 创建订单
this.save(orders);

//构建快照
OrderSnapshotDTO orderSnapshotDTO = BeanUtil.toBean(this.getById(orders.getId()), OrderSnapshotDTO.class);

//启动状态机
orderStateMachine.start(null,orders.getId().toString(),orderSnapshotDTO);
}