状态机
什么是状态机
需求说明
在订单模块中订单共设计了7种状态,如下图

订单会在多个状态间进行转换,这就需要开发人员都去记忆这些转换关系,这是非常麻烦的
if(订单状态 == 待支付){
//如果用户支付成功,执行此场景下的业务逻辑,更新订单状态为派单中
update(id,派单中)
)
if(订单状态 == 待支付){
//如果用户取消支付,执行此场景下的业务逻辑。更新订单状态为已取消
update(id,已取消)
)
...
针对这种情况,可以引入状态机对订单状态进行统一管理,可以认为状态机就是一个封装好的组件
调用它的时候,只需要告诉它订单id和要执行的事件,它内部就可以完成对应的所有操作,类似于下面代码
//调用状态机执行支付成功事件
orderStateMachine.changeStatus(订单id,支付成功事件);
//调用状态机执行支付取消事件
orderStateMachine.changeStatus(订单id,支付取消事件);
因此使用状态机的好处就是:
- 易于理解:可以使业务模型清晰,开发人员可以更好地理解状态转换流程
- 方便调用:可以使调用者以不关注细节的角度去使用其暴露的接口方法
当然使用状态机也是有缺点的:
- 代码复杂:状态机需要较多接口和实现类,因此代码复杂度会高一点
- 运行效率:状态机需要经常创建状态机实例,运行效率会稍微差一点
状态机介绍
状态机是一种抽象的数学模型,用于描述事物在不同状态之间转移和行为变化的过程
它的核心是将状态之间的变更定义为事件,然后将事件暴露出来,通过执行状态变更事件去更改状态
理解状态机设计模式需要理解四个要素:现态、次态、事件、动作
- 现态:是指当前所处的状态,比如说下图的待支付
- 次态:条件满足后要转变为的新状态,比如说下图的派单中
- 事件:状态变更的触发条件,比如说下图的用户支付成功事件
- 动作:事件发生时执行的操作,将订单状态由待支付更改为派单中,它不是必需的
拿待支付状态到派单中状态举例

编写订单状态机
实现状态机步骤比较复杂,核心步骤如下:
- 引入通用状态机组件,内部定义了一些类和接口,订单状态机要去继承或实现这些类和接口
- 创建状态类,定义订单所有的状态,里面就包含所有现态和次态
- 创建事件类,定义触发订单状态改变的事件,它关联现态和次态
- 创建快照类,用来记录事件发生时,订单变化瞬间的状态及相关信息
- 创建动作类,用来执行订单状态发生改变需要触发的操作
- 创建状态机类,定义状态机的名称和初始状态
添加依赖
在jzo2o-orders-base工程的pom.xml中添加状态机组件的依赖
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-statemachine</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
创建订单状态类
定义订单所有的状态,实现接口StatusDefine

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

创建订单快照类
用来记录事件发生时,订单变化瞬间的状态及相关信息,继承StateMachineSnapshot,例如:
- 001号订单创建成功,此时会记录它的快照信息(订单号、下单人、订单详细信息、订单状态等)
- 001号订单支付成功,此时也会记录它的快照信息
订单快照可以追溯订单的历史变化信息,只要状态发生变化便会记录快照

创建订单动作类
用来执行订单状态发生改变需要触发的操作,实现StatusChangeHandler
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
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状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类

在整个状态机运转过程中需要两张数据表来记录数据,分别是
- 状态机表:每个订单在此表中有一条数据,里面存储订单的最新状态
- 状态机快照表:每个订单在此表有多条记录,里面存储的是订单到现在为止历经的状态

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

测试启动状态机
在jzo2o-orders-manager下编写测试代码
调用OrderStateMachine的start()方法启动一个订单的状态机,它会设置订单的初始状态
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号订单的状态持久化记录

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

注意:如果报错:java.lang.IllegalStateException: 已存在状态,不可初始化 ,说明888号订单的状态机已启动,可以更改101测试其它订单状态机启动
源码阅读
进入
jzo2o-framework下的jzo2o-statemachine工程,阅读AbstractStateMachine类的源码通过阅读代码理解状态机组件的运行过程
start-启动状态机
- 向状态机表保存一条记录
- 向状态机快照表保存一条记录
changeStatus-状态变更
- 从容器中根据 状态机名字_事件名称 找到一个对象, 调用对象的handler方法
- 更新状态机表中的状态字段
- 向状态机快照表保存一条记录
启动状态机
先判断该订单是否启动状态,如果没有启动则向状态机表插入记录,否则抛出异常”已存在状态,不可初始化”.
/**
* 状态机初始化,不保存快照
* @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();
}
状态变更
状态变更前会判断订单的当前状态是否和事件定义的源状态一致
如果不一致则说明当前订单的状态不能通过该事件去更新状态,此时将终止状态变更
否则将通过状态变更处理器更新订单的状态
/**
* 变更状态并保存快照,快照不进行分库
*
* @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类中保存订单的方法,添加启动状态机的逻辑
注意:状态机的操作方法要和业务方法处于一个事务中
@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);
}