使用状态机实现授权状态变更

状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。

状态机设计基本原则

  1. 明确性:状态和转换必须清晰定义,避免含糊不清的状态。
  2. 完备性:为所有可能的事件-状态组合定义转换逻辑。
  3. 可预测性:系统应根据当前状态和给定事件可预测地响应。
  4. 最小化:状态数应保持最小,避免不必要的复杂性。
  5. 可扩展性:状态机应该具有良好的可扩展性,以便在业务需求变化时能够方便地添加新的状态和转换规则

状态机常见设计误区

  1. 过度设计引入不必要的状态和复杂性,使系统难以理解和维护。
  2. 不完备的处理:未能处理所有可能的状态转换,导致系统行为不确定。
  3. 硬编码逻辑:过多的硬编码转换逻辑,使系统不具备灵活性和可扩展性。

状态机设计的最佳实践

  1. 分离状态和处理逻辑:使用状态模式,将每个状态的行为封装在各自的类中。
  2. 使用事件驱动模型:通过事件来触发状态转换,而不是直接调用状态方法。
  3. 确保可追踪性:状态转换应该能被记录和追踪,以便于故障排查和审计。

Spring Statemachine核心思路

  1. State ,状态。一个状态机至少要包含两个或以上的状态。状态与状态之间可以转换。
  2. Event ,事件。事件就是执行状态转换的触发条件。
  3. Action ,动作。事件发生以后要执行动作。
  4. 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);
    }
}

总结

  1. 状态机一定要设计好,只有特定的原始状态 + 特定的事件才可以推进到指定的状态。
  2. 更新数据库之前,先使用select for update进行锁行记录,同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束,更新失败就组成消息发到消息队列,后面再消费。
  3. 通过补偿机制兜底,比如查询补单。
  4. 通过上述三个步骤,正常情况下,最终的数据状态一定是正确的。除非是某个系统有异常,这样只能进人工差错处理流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值