状态机的基本使用
状态机
1. 什么是状态机
1.1 场景
在业务代码中对一些业务状态进行硬编码,如果有一天更改了业务逻辑就需要更改代码,不方便进行系统扩展和维护。
if (status == 状态1) {
// TODO
} else if(status == 状态2) {
// TODO
} ...
另外对订单状态的管理是散落在很多地方不方便对订单状态进行统一管理和维护。
1.2 使用状态机解决问题
使用状态机对业务状态进行统一管理。
理解状态机设计模式需要理解四个要素:
- 现态:是指当前所处的状态;
- 事件:触发状态变更的事件;
- 动作:当事件被触发时,执行的操作;
- 次态:条件满足后要迁往的新状态。
例如:拿待支付状态到派单中状态举例:
![]()
- 现态:订单当前处于待支付状态那么现态为待支付。
- 事件:用户支付成功为事件,支付成功是条件,当条件满足进行状态迁移。
- 动作:将订单状态由待支付更改为派单中。
- 次态:派单中。
2. 实现状态机
基于设计模式开发状态机组件,参考代码:statemachine.zip
在需要使用的模块中添加状态机依赖
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-statemachine</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
下面以订单业务进行举例
2.1 状态枚举类
阅读订单状态枚举类,实现了 StatusDefine 状态接口,不论是现态还是次态都需要实现状态接口。
@Getter
@AllArgsConstructor
public enum OrderStatusEnum implements StatusDefine {
NO_PAY(0, "待支付", "NO_PAY"),
DISPATCHING(100, "派单中", "DISPATCHING"),
NO_SERVE(200, "待服务", "NO_SERVE"),
SERVING(300, "服务中", "SERVING"),
FINISHED(500, "已完成", "FINISHED"),
CANCELED(600, "已取消", "CANCELED"),
CLOSED(700, "已关闭", "CLOSED");
private final Integer status;
private final String desc;
private final String code;
// 根据状态值获得对应枚举
public static OrderStatusEnum codeOf(Integer status) {
for (OrderStatusEnum orderStatusEnum : values()) {
if (orderStatusEnum.status.equals(status)) { return orderStatusEnum; }
}
return null;
}
}
2.2 状态变更事件枚举类
所有状态之间存在的变更都需要定义状态变更事件,它实现了 StatusChangeEvent 状态变更事件接口,事件对应状态机四要素的事件
@Getter
@AllArgsConstructor
public enum OrderStatusChangeEventEnum implements StatusChangeEvent {
PAYED(OrderStatusEnum.NO_PAY, OrderStatusEnum.DISPATCHING, "支付成功", "payed"),
DISPATCH(OrderStatusEnum.DISPATCHING, OrderStatusEnum.NO_SERVE, "接单/抢单成功", "dispatch"),
START_SERVE(OrderStatusEnum.NO_SERVE, OrderStatusEnum.SERVING, "开始服务", "start_serve"),
COMPLETE_SERVE(OrderStatusEnum.SERVING, OrderStatusEnum.FINISHED, "完成服务", "complete_serve"),
EVALUATE(OrderStatusEnum.NO_EVALUATION, OrderStatusEnum.FINISHED, "评价完成", "evaluate"),
CANCEL(OrderStatusEnum.NO_PAY, OrderStatusEnum.CANCELED, "取消订单", "cancel"),
SERVE_PROVIDER_CANCEL(OrderStatusEnum.NO_SERVE, OrderStatusEnum.DISPATCHING, "服务人员/机构取消订单", "serve_provider_cancel"),
CLOSE_DISPATCHING_ORDER(OrderStatusEnum.DISPATCHING, OrderStatusEnum.CLOSED, "派单中订单关闭", "close_dispatching_order"),
CLOSE_NO_SERVE_ORDER(OrderStatusEnum.NO_SERVE, OrderStatusEnum.CLOSED, "待服务订单关闭", "close_no_serve_order"),
CLOSE_SERVING_ORDER(OrderStatusEnum.SERVING, OrderStatusEnum.CLOSED, "服务中订单关闭", "close_serving_order"),
CLOSE_NO_EVALUATION_ORDER(OrderStatusEnum.NO_EVALUATION, OrderStatusEnum.CLOSED, "待评价订单关闭", "close_no_evaluation_order"),
CLOSE_FINISHED_ORDER(OrderStatusEnum.FINISHED, OrderStatusEnum.CLOSED, "已完成订单关闭", "close_finished_order");
// 源状态
private final OrderStatusEnum sourceStatus;
// 目标状态
private final OrderStatusEnum targetStatus;
// 描述
private final String desc;
// 代码
private final String code;
}
2.3 定义订单快照类
快照是订单变化瞬间的状态及相关信息。
快照基础类型是 StateMachineSnapshot,如果我们要实现订单快照则需要定义一个订单快照类 OrderSnapshotDTO 去继承 StateMachineSnapshot 类型,代码如下:
// 订单快照@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSnapshotDTO extends StateMachineSnapshot {
// ...原来的内容保持不变,添加以下代码
@Override
public String getSnapshotId() { return String.valueOf(id); }
@Override
public Integer getSnapshotStatus() { return ordersStatus; }
@Override
public void setSnapshotId(String snapshotId) {
this.id = Long.parseLong(snapshotId);
}
@Override
public void setSnapshotStatus(Integer snapshotStatus) {
this.ordersStatus = snapshotStatus;
}
}
2.4 定义事件变更动作类
当执行状态变更事件会伴随着执行具体的动作,此部分对应状态机四要素中的动作。
// 订单支付成功处理器
@Slf4j
@Component("order_payed")
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {
@Resource
private IOrdersCommonService ordersService;
/**
* 订单支付处理逻辑
*
* @param bizId 业务id
* @param bizSnapshot 快照
*/
@Override
public void handler(String bizId, StatusChangeEvent statusChangeEventEnum, OrderSnapshotDTO bizSnapshot) {
log.info("支付成功事件处理逻辑开始,订单号:{}", bizId);
}
}
2.5 定义订单状态机类
AbstractStateMachine 状态机抽象类是状态机的核心类,是具体的状态机要继承的抽象类,比如我们实现订单状态机就需要继承 AbstractStateMachine 抽象类。
// 订单状态机
@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"; }
@Override
protected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) { }
/**
* 设置状态机初始状态
*
* @return 状态机初始状态
*/
@Override
protected OrderStatusEnum getInitState() { return OrderStatusEnum.NO_PAY; }
}
2.6 状态机表设计
-
状态机持久化表:
每个订单对应状态机表中的一条记录。
CREATE TABLE `state_persister` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `state_machine_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态机名称', `biz_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '业务id', `state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `唯一索引` (`state_machine_name`,`biz_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1908702574605910019 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='状态机持久化表';
-
状态机快照表:
一个订单在快照表有多条记录,每变一个状态会记录该状态下的快照信息(即订单相关的详细信息)便于查询订单变化的历史记录。
CREATE TABLE `biz_snapshot` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `state_machine_name` varchar(50) DEFAULT NULL COMMENT '状态机名称', `biz_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '业务id', `db_shard_id` bigint DEFAULT NULL COMMENT '分库键', `state` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '状态代码', `biz_data` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '业务数据', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1908702660589142017 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='业务数据快照';
3. 使用
在配置文件中导入状态机
@Configuration
@ComponentScan({"com.jzo2o.orders.base.service","com.jzo2o.orders.base.handler"})
@MapperScan("com.jzo2o.orders.base.mapper")
@Import({OrderStateMachine.class}) // 导入状态机
@EnableConfigurationProperties({DispatchProperties.class, ExecutorProperties.class})
public class AutoImportConfiguration { }
启用订单状态机:
// 创建订单快照对象
OrderSnapshotDTO orderSnapshotDTO = BeanUtils.toBean(orders, OrderSnapshotDTO.class);
/*
* 启动订单状态机
*
* Long dbShardId :分库ID
* String bizId :订单ID
* StatusDefine statusDefine :订单状态定义(默认 NO_PAY,可省略)
* T bizSnapshot :订单快照
*/
orderStateMachine.start(ordersId.toString(), OrderStatusEnum.NO_PAY, orderSnapshotDTO);
调用状态机:
String bizId = orders.getId().toString();
// 创建快照对象,可配置需要的数据
OrderSnapshotDTO orderSnapshotDTO = new OrderSnapshotDTO();
// orderSnapshotDTO.setPayTime(DateUtils.now());
// ...
// 调用状态机,更新订单状态
orderStateMachine.changeStatus(bizId, OrderStatusChangeEventEnum.PAYED, orderSnapshotDTO);
说明:这里使用状态变更事件未 PAYED,参考 事件变更枚举类 可以看到运行的代码为 payed,于是就可以找到对于事件处理器 order_payed ,从而处理对应的事件。