什么是状态机
需求说明
在订单模块中订单共设计了7种状态,如下图
订单会在多个状态间进行转换,这就需要开发人员都去记忆这些转换关系,这是非常麻烦的
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,支付取消事件);
|
因此使用状态机的好处就是:
- 易于理解:可以使业务模型清晰,开发人员可以更好地理解状态转换流程
- 方便调用:可以使调用者以不关注细节的角度去使用其暴露的接口方法
当然使用状态机也是有缺点的:
- 代码复杂:状态机需要较多接口和实现类,因此代码复杂度会高一点
- 运行效率:状态机需要经常创建状态机实例,运行效率会稍微差一点
状态机介绍
状态机是一种抽象的数学模型,用于描述事物在不同状态之间转移和行为变化的过程
它的核心是将状态之间的变更定义为事件,然后将事件暴露出来,通过执行状态变更事件去更改状态
理解状态机设计模式需要理解四个要素:现态、次态、事件、动作
- 现态:是指当前所处的状态,比如说下图的待支付
- 次态:条件满足后要转变为的新状态,比如说下图的派单中
- 事件:状态变更的触发条件,比如说下图的用户支付成功事件
- 动作:事件发生时执行的操作,将订单状态由待支付更改为派单中,它不是必需的
拿待支付状态到派单中状态举例

编写订单状态机
实现状态机步骤比较复杂,核心步骤如下:
- 引入通用状态机组件,内部定义了一些类和接口,订单状态机要去继承或实现这些类和接口
- 创建状态类,定义订单所有的状态,里面就包含所有现态和次态
- 创建事件类,定义触发订单状态改变的事件,它关联现态和次态
- 创建快照类,用来记录事件发生时,订单变化瞬间的状态及相关信息
- 创建动作类,用来执行订单状态发生改变需要触发的操作
- 创建状态机类,定义状态机的名称和初始状态
添加依赖
在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

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

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

创建订单动作类
用来执行订单状态发生改变需要触发的操作,实现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") public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {
@Autowired private IOrdersCommonService ordersService;
@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); }
@Override protected String getName() { return "order"; }
@Override protected OrderStatusEnum getInitState() { return OrderStatusEnum.NO_PAY; }
@Override protected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) {
} }
|
AbstractStateMachine状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类

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

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

测试启动状态机
在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() { 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方法
- 更新状态机表中的状态字段
- 向状态机快照表保存一条记录
启动状态机
先判断该订单是否启动状态,如果没有启动则向状态机表插入记录,否则抛出异常”已存在状态,不可初始化”.
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
|
public String start(String bizId) { return start(null, bizId, initState, null); }
public String start(Long dbShardId, String bizId, StatusDefine statusDefine, T bizSnapshot) {
String currentState = stateMachinePersister.getCurrentState(name, bizId); if (ObjectUtil.isEmpty(currentState)) { stateMachinePersister.init(name, bizId, statusDefine); } else { throw new IllegalStateException("已存在状态,不可初始化"); }
if (bizSnapshot == null) { bizSnapshot = ReflectUtil.newInstance(getSnapshotClass()); } bizSnapshot.setSnapshotId(bizId); bizSnapshot.setSnapshotStatus(statusDefine.getStatus()); 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
|
public void changeStatus(String bizId, StatusChangeEvent statusChangeEventEnum) { changeStatus(null, bizId, statusChangeEventEnum, null); }
public void changeStatus(Long dbShardId, String bizId, StatusChangeEvent statusChangeEventEnum, T bizSnapshot) { String statusCode = getCurrentState(bizId);
if (ObjectUtil.isNotEmpty(statusChangeEventEnum.getSourceStatus()) && ObjectUtil.notEqual(statusChangeEventEnum.getSourceStatus().getCode(), statusCode)) { throw new CommonException(HTTP_INTERNAL_ERROR, "状态机起止状态与事件不匹配"); }
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()); } bizSnapshot.setSnapshotId(bizId); bizSnapshot.setSnapshotStatus(statusChangeEventEnum.getTargetStatus().getStatus()); if (ObjectUtil.isNotNull(bean)) { bean.handler(bizId, statusChangeEventEnum, bizSnapshot); }
stateMachinePersister.persist(name, bizId, statusChangeEventEnum.getTargetStatus());
if (ObjectUtil.isNotEmpty(bizSnapshot)) { bizSnapshot = buildNewSnapshot(bizId, bizSnapshot, statusChangeEventEnum.getSourceStatus()); String newBizSnapShotString = JSONUtil.toJsonStr(bizSnapshot); bizSnapshotService.save(dbShardId, name, bizId, statusChangeEventEnum.getTargetStatus(), newBizSnapShotString); }
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) { CouponUseReqDTO couponUseReqDTO = new CouponUseReqDTO(); couponUseReqDTO.setId(couponId); couponUseReqDTO.setOrdersId(orders.getId()); couponUseReqDTO.setTotalAmount(orders.getTotalAmount()); CouponUseResDTO couponUseResDTO = couponApi.use(couponUseReqDTO);
BigDecimal discountAmount = couponUseResDTO.getDiscountAmount(); orders.setDiscountAmount(discountAmount); orders.setRealPayAmount(orders.getTotalAmount().subtract(discountAmount));
this.save(orders);
OrderSnapshotDTO orderSnapshotDTO = BeanUtil.toBean(this.getById(orders.getId()), OrderSnapshotDTO.class); orderStateMachine.start(null,orders.getId().toString(),orderSnapshotDTO); }
|