狀態機
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 {// ...原來的內容保持不變,添加以下代碼@Overridepublic String getSnapshotId() { return String.valueOf(id); }@Overridepublic Integer getSnapshotStatus() { return ordersStatus; }@Overridepublic void setSnapshotId(String snapshotId) {this.id = Long.parseLong(snapshotId);}@Overridepublic void setSnapshotStatus(Integer snapshotStatus) {this.ordersStatus = snapshotStatus;}
}
2.4 定義事件變更動作類
當執行狀態變更事件會伴隨著執行具體的動作,此部分對應狀態機四要素中的動作。
// 訂單支付成功處理器
@Slf4j
@Component("order_payed")
public class OrderPayedHandler implements StatusChangeHandler<OrderSnapshotDTO> {@Resourceprivate IOrdersCommonService ordersService;/*** 訂單支付處理邏輯** @param bizId 業務id* @param bizSnapshot 快照*/@Overridepublic 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 狀態機名稱*/@Overrideprotected String getName() { return "order"; }@Overrideprotected void postProcessor(OrderSnapshotDTO orderSnapshotDTO) { }/*** 設置狀態機初始狀態** @return 狀態機初始狀態*/@Overrideprotected 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 ,從而處理對應的事件。