状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。
状态机设计基本原则
- 明确性:状态和转换必须清晰定义,避免含糊不清的状态。
- 完备性:为所有可能的事件-状态组合定义转换逻辑。
- 可预测性:系统应根据当前状态和给定事件可预测地响应。
- 最小化:状态数应保持最小,避免不必要的复杂性。
- 可扩展性:状态机应该具有良好的可扩展性,以便在业务需求变化时能够方便地添加新的状态和转换规则
状态机常见设计误区
状态机设计的最佳实践
Spring Statemachine核心思路
- State ,状态。一个状态机至少要包含两个或以上的状态。状态与状态之间可以转换。
- Event ,事件。事件就是执行状态转换的触发条件。
- Action ,动作。事件发生以后要执行动作。
- Transition ,变换。也就是从一个状态变化为另一个状态。
/**
* 状态基类
*/
public interface BaseStatus {
}
/**
* 事件基类
*/
public interface BaseEvent {
}
/**
* 授权状态机
*/
public enum AuthStatus implements BaseStatus {
INIT("INIT", "授权创建"),
SUCCEED("SUCCEED", "授权成功"),
BIND("BIND", "绑定成功"),
FAILED("FAILED", "授权失败");
// 授权状态机内容
private static final StateMachine<AuthStatus, AuthEvent> STATE_MACHINE = new StateMachine<>();
static {
// 初始状态
STATE_MACHINE.accept(null, AuthEvent.AUTH_CREATE, INIT);
// 授权成功
STATE_MACHINE.accept(INIT, AuthEvent.AUTH_SUCCESS, SUCCEED);
// 绑定成功
STATE_MACHINE.accept(SUCCEED, AuthEvent.BIND_SUCCESS, BIND);
// 授权失败
STATE_MACHINE.accept(INIT, AuthEvent.AUTH_FAIL, FAILED);
}
// 状态
private final String status;
// 描述
private final String description;
AuthStatus(String status, String description) {
this.status = status;
this.description = description;
}
/**
* 通过源状态和事件类型获取目标状态
*/
public static AuthStatus getTargetStatus(AuthStatus sourceStatus, AuthEvent event) {
return STATE_MACHINE.getTargetStatus(sourceStatus, event);
}
}
/**
* 授权事件
*/
public enum AuthEvent implements BaseEvent {
// 授权创建
AUTH_CREATE("AUTH_CREATE", "授权创建"),
// 授权成功
AUTH_SUCCESS("AUTH_SUCCESS", "授权成功"),
// 绑定成功
BIND_SUCCESS("BIND_SUCCESS", "绑定成功"),
// 授权失败
AUTH_FAIL("AUTH_FAIL", "授权失败");
/**
* 事件
*/
private String event;
/**
* 事件描述
*/
private String description;
AuthEvent(String event, String description) {
this.event = event;
this.description = description;
}
}
import java.util.HashMap;
import java.util.Map;
/**
* 状态机
*/
public class StateMachine<S extends BaseStatus, E extends BaseEvent> {
private final Map<StatusEventPair<S, E>, S> statusEventMap = new HashMap<>();
/**
* 只接受指定的当前状态下,指定的事件触发,可以到达的指定目标状态
*/
public void accept(S sourceStatus, E event, S targetStatus) {
statusEventMap.put(new StatusEventPair<>(sourceStatus, event), targetStatus);
}
/**
* 通过源状态和事件,获取目标状态
*/
public S getTargetStatus(S sourceStatus, E event) {
return statusEventMap.get(new StatusEventPair<>(sourceStatus, event));
}
}
/**
* 授权模型
*/
public class AuthModel {
// 上次状态
private AuthStatus lastStatus;
// 当前状态
private AuthStatus currentStatus;
/**
* 根据事件推进状态
*/
public void transferStatusByEvent(AuthEvent event) throws StateMachineException {
// 根据当前状态和事件,去获取目标状态
AuthStatus targetStatus = AuthStatus.getTargetStatus(currentStatus, event);
// 如果目标状态不为空,说明是可以推进的
if (targetStatus != null) {
lastStatus = currentStatus;
currentStatus = targetStatus;
} else {
// 目标状态为空,说明是非法推进,进入异常处理,这里只是抛出去,由调用者去具体处理
throw new StateMachineException(currentStatus, event, "状态转换失败");
}
}
}
public class StateMachineException extends Exception {
private AuthStatus currentStatus;
private AuthEvent event;
public StateMachineException(AuthStatus currentStatus, AuthEvent event, String message) {
super(message);
this.currentStatus = currentStatus;
this.event = event;
}
public AuthStatus getCurrentStatus() {
return currentStatus;
}
public AuthEvent getEvent() {
return event;
}
@Override
public String toString() {
return "StateMachineException{" +
"currentStatus='" + currentStatus + '\'' +
", event='" + event + '\'' +
", message='" + getMessage() + '\'' +
'}';
}
}
import java.util.Objects;
/**
* 状态事件对,指定的状态只能接受指定的事件
*/
public class StatusEventPair<S extends BaseStatus, E extends BaseEvent> {
/**
* 指定的状态
*/
private final S status;
/**
* 可接受的事件
*/
private final E event;
public StatusEventPair(S status, E event) {
this.status = status;
this.event = event;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof StatusEventPair) {
StatusEventPair<S, E> other = (StatusEventPair<S, E>)obj;
return this.status.equals(other.status) && this.event.equals(other.event);
}
return false;
}
@Override
public int hashCode() {
// 后续使用google的guava包。com.google.common.base.Objects。lichun-todo
return Objects.hash(status, event);
}
}
总结
- 状态机一定要设计好,只有特定的原始状态 + 特定的事件才可以推进到指定的状态。
- 更新数据库之前,先使用select for update进行锁行记录,同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束,更新失败就组成消息发到消息队列,后面再消费。
- 通过补偿机制兜底,比如查询补单。
- 通过上述三个步骤,正常情况下,最终的数据状态一定是正确的。除非是某个系统有异常,这样只能进人工差错处理流程。