怎么才能更好的管理订单的生命周期

74 阅读9分钟

欢迎关注我的公众号:Java开发者笔记

扫码_搜索联合传播样式-白色版.png

业务背景

最近在我们新改造的财务项目中,对于以往的财务凭证状态改变的编码和设计有了不同的见解;

在一张专业财务凭证中,订单的状态会经过如下状态:

image.png

内部复杂的状态流转,第一眼看到下意思的就会觉得,这个业务简单,几个状态枚举,加上几个不同的接口就完事儿了嘛,比如你会写这样的代码:

@RestController
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    /**
    * 创建订单
    */
    @PostMapping("create")
    public Result createOrder(@RequestBody OrderDto order) {
        return orderService.create(order);
    }
    
    /**
    * 审核订单
    */
    @PostMapping("auditOrder")
    public Result auditOrder(@RequestBody OrderDto order) {
        return orderService.auditOrder(order);
    }
    
    /**
    * 审核驳回订单
    */
    @PostMapping("auditBackOrder")
    public Result auditBackOrder(@RequestBody OrderDto order) {
        return orderService.auditBackOrder(order);
    }
    
    // ...等等接口
    
}

不是说通过接口不能实现这个业务,只是接口带来的是线程的开销,据我了解身边在大厂的朋友,他们每写一个接口都需要经过复杂的审核,这背后的思考就是服务器的资源压力问题;

那除了上述通过接口来控制每一个状态,还有没有什么其他的办法?

  • 策略模式
  • 状态模式

策略模式和状态模式均可用于这种状态变化的事件,只不过有一点内部的区别:

  • 策略模式重点关注在于对象的不同策略之间的不同选择,重点是选择;
  • 状态模式重点在于对象的状态不同在不同事件下的不同流转,重点是状态;

所以我们最终采用了状态模式来完成这次的业务改造;

经过阅读 Spring 官网得知,Spring 提供一种工具,Spring State Machine,意思就是Spring 状态机,能完美解决上述业务中的状态流转问题:

image.png

一整段英文介绍中,重点的描述就是:应用程序处于有限数量的状态,但是随着某些事件的发生,会将应用程序从一个状态切换到另一个状态;

Spring 状态机定义

状态机,不是一种框架,也不是一个机器,它更抽象于是一种数学模型;更简单的一点可以说是一种管理状态流转的图;

从状态变化这四个字可以看出,一个状态机会包含以下几种要素:

  • 原始状态:指的是一个对象在未变化之前的状态,例如草稿状态
  • 事件:触发这个对象由一个事件变到另一个状态的动作,称之为事件
  • 响应回调:事件触发后,对应的回调函数用于执行后续业务
  • 目标状态:对象需要转换到的状态
  • 状态守卫:一个状态流转到另一个状态时的监控,可以让状态流转成功或者流转失败;

技术实现

环境准备

采用Spring boot 2.7.18 + jdk 1.8 + spring-statemachine-core 3.2.1,pom如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.20</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.statemachine</groupId>
        <artifactId>spring-statemachine-core</artifactId>
        <version>3.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.0</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
</dependencies>

然后创建一个订单表和订单日志表:

-- 订单表
CREATE TABLE voucher_order
(
    id           BIGINT         NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    order_no     VARCHAR(64)    NOT NULL COMMENT '订单号',
    tenant_id    BIGINT         NOT NULL COMMENT '租户ID',
    order_amount DECIMAL(10, 2) NOT NULL COMMENT '订单金额',
    order_state  VARCHAR(32)    NOT NULL COMMENT '订单状态(DRAFT/AUDITING等)',
    order_date   DATE           NOT 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),
    UNIQUE KEY uk_order_no (order_no)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4 COMMENT ='订单表';

-- 状态流转日志表
CREATE TABLE voucher_state_log
(
    id          BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    tenant_id   BIGINT       NOT NULL COMMENT '租户ID',
    order_id    BIGINT       NOT NULL COMMENT '订单ID',
    log_content VARCHAR(512) NOT 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),
    KEY idx_order_id (order_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4 COMMENT ='订单状态流转日志表';

-- 测试数据(草稿状态订单)
INSERT INTO voucher_order (order_no, tenant_id, order_amount, order_state, order_date)
VALUES ('TEST20251027001', 1, 999.99, 'DRAFT', '2025-10-27');

-- 状态机持久化表(存储每个订单的当前状态)
CREATE TABLE state_machine_persist
(
    order_id    BIGINT      NOT NULL COMMENT '订单ID(外键)',
    state       VARCHAR(32) NOT 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 (order_id),
    CONSTRAINT fk_sm_order FOREIGN KEY (order_id) REFERENCES voucher_order (id) ON DELETE CASCADE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4 COMMENT ='状态机持久化表';

状态枚举和事件枚举

在准备好上述的pom和sql文件后,我们根据上述的状态流转图,来定义对应的状态枚举和事件枚举:

public enum VoucherState {
    // 草稿
    DRAFT,
    // 审核中
    AUDITING,
    // 审核通过
    AUDIT_PASSED,
    // 核算
    ACCOUNTING,
    // 红冲
    RED_FLUSH,
    // 归档
    ARCHIVED
}

public enum VoucherEvent {
    // 提交审核(草稿->审核中)
    SUBMIT_AUDIT,
    // 审核通过(审核中->审核通过)
    AUDIT_SUCCESS,
    // 驳回审核(审核通过->草稿)
    REJECT_AUDIT,
    // 进入核算(审核通过->核算)
    ENTER_ACCOUNTING,
    // 执行归档(核算->归档)
    ARCHIVE,
    // 执行红冲(核算->红冲)
    RED_FLUSH
}

状态机核心配置

上面说到,状态机一共需要五个部分组成,我们定义好了状态枚举和事件枚举后,接下来就是配置状态机的状态流转过程,只能由某个状态流转到某个状态:

@Configuration
@EnableStateMachineFactory
public class VoucherStateMachineConfig extends StateMachineConfigurerAdapter<VoucherState, VoucherEvent> {

    @Autowired
    private StateMachinePersistService persister;
    @Autowired
    private VoucherStateGuard guard;
    @Autowired
    private VoucherStateListener listener;

    // 配置状态
    @Override
    public void configure(StateMachineStateConfigurer<VoucherState, VoucherEvent> states) throws Exception {
        states
            .withStates()
            .initial(VoucherState.DRAFT) // 初始状态:草稿
            .states(EnumSet.allOf(VoucherState.class));
    }

    // 配置状态流转
    @Override
    public void configure(StateMachineTransitionConfigurer<VoucherState, VoucherEvent> transitions) throws Exception {
        transitions
            // 草稿 -> 审核中(提交审核)
            .withExternal()
            .source(VoucherState.DRAFT).target(VoucherState.AUDITING)
            .event(VoucherEvent.SUBMIT_AUDIT)
            .guard(guard)
            .and()
            // 审核中 -> 审核通过(审核通过)
            .withExternal()
            .source(VoucherState.AUDITING).target(VoucherState.AUDIT_PASSED)
            .event(VoucherEvent.AUDIT_SUCCESS)
            .guard(guard)
            .and()
            // 审核通过 -> 草稿(驳回审核)
            .withExternal()
            .source(VoucherState.AUDIT_PASSED).target(VoucherState.DRAFT)
            .event(VoucherEvent.REJECT_AUDIT)
            .guard(guard)
            .and()
            // 审核通过 -> 核算(进入核算)
            .withExternal()
            .source(VoucherState.AUDIT_PASSED).target(VoucherState.ACCOUNTING)
            .event(VoucherEvent.ENTER_ACCOUNTING)
            .guard(guard)
            .and()
            // 核算 -> 归档(执行归档)
            .withExternal()
            .source(VoucherState.ACCOUNTING).target(VoucherState.ARCHIVED)
            .event(VoucherEvent.ARCHIVE)
            .guard(guard)
            .and()
            // 核算 -> 红冲(执行红冲)
            .withExternal()
            .source(VoucherState.ACCOUNTING).target(VoucherState.RED_FLUSH)
            .event(VoucherEvent.RED_FLUSH)
            .guard(guard);
    }

    // 配置监听器
    @Override
    public void configure(StateMachineConfigurationConfigurer<VoucherState, VoucherEvent> config) throws Exception {
        config
            .withConfiguration()
            .listener(listener); // 全局事件监听器
    }

    // 持久化配置(使用内存存储,实际生产可替换为Redis等)
    @Bean
    public StateMachinePersister<VoucherState, VoucherEvent, Long> persister() {
        return persister;
    }
}

并且状态机给我们提供了事件监听器,用来监听从某个状态流转到某个状态后的事件处理:

@Component
public class VoucherStateListener implements StateMachineListener<VoucherState, VoucherEvent> {

    @Autowired
    private VoucherOrderMapper orderMapper;
    @Autowired
    private VoucherStateLogMapper logMapper;


    // 用于临时存储状态上下文(替代原StateMachineListenerAdapter的方式)
    private final ThreadLocal<StateContext<VoucherState, VoucherEvent>> contextHolder = new ThreadLocal<>();


    @Override
    public void stateContext(StateContext<VoucherState, VoucherEvent> stateContext) {
        this.contextHolder.set(stateContext);
    }
    @Override
    public void stateChanged(State<VoucherState, VoucherEvent> from, State<VoucherState, VoucherEvent> to) {
        // 状态变更时执行(初始状态不记录)
        if (from != null) {
            // 获取上下文参数
            StateContext<VoucherState, VoucherEvent> context = contextHolder.get();
            Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
            VoucherOrder order = orderMapper.selectById(orderId);

            // 1. 更新订单状态
            order.setOrderState(to.getId().name());
            orderMapper.updateById(order);

            // 2. 记录状态流转日志
            VoucherStateLog log = new VoucherStateLog();
            log.setOrderId(orderId);
            log.setTenantId(order.getTenantId());
            log.setLogContent(String.format("状态从[%s]流转至[%s]", from.getId(), to.getId()));
            logMapper.insert(log);
        }
    }

    // 其他默认方法(可根据需要实现)
    @Override
    public void stateEntered(State<VoucherState, VoucherEvent> state) {}
    @Override
    public void stateExited(State<VoucherState, VoucherEvent> state) {}
    @Override
    public void eventNotAccepted(Message<VoucherEvent> event) {}
    @Override
    public void transition(Transition<VoucherState, VoucherEvent> transition) {}
    @Override
    public void transitionStarted(Transition<VoucherState, VoucherEvent> transition) {}
    @Override
    public void transitionEnded(Transition<VoucherState, VoucherEvent> transition) {}
    @Override
    public void stateMachineStarted(StateMachine<VoucherState, VoucherEvent> stateMachine) {}
    @Override
    public void stateMachineStopped(StateMachine<VoucherState, VoucherEvent> stateMachine) {}
    @Override
    public void stateMachineError(StateMachine<VoucherState, VoucherEvent> stateMachine, Exception exception) {}
    @Override
    public void extendedStateChanged(Object key, Object value) {}

}

并且在配置过程中,需要考虑服务宕机的情况,所以需要一个持久化的操作,这里你可以选择使用一张持久化表,也可以选择使用订单本身字段来维护持久化状态:

@Service
public class StateMachinePersistService implements StateMachinePersister<VoucherState, VoucherEvent, Long> {

    @Autowired
    private StateMachineMapper stateMachineMapper;

    @Override
    public void persist(StateMachine<VoucherState, VoucherEvent> stateMachine, Long orderId) throws Exception {
        // 保存当前状态到数据库
        StateMachineEntity entity = new StateMachineEntity();
        entity.setOrderId(orderId);
        entity.setState(stateMachine.getState().getId().name());
        
        // 存在则更新,不存在则插入
        if (stateMachineMapper.selectById(orderId) != null) {
            stateMachineMapper.updateById(entity);
        } else {
            stateMachineMapper.insert(entity);
        }
    }

    @Override
    public StateMachine<VoucherState, VoucherEvent> restore(StateMachine<VoucherState, VoucherEvent> stateMachine, Long orderId) throws Exception {
        // 从数据库恢复状态
        StateMachineEntity entity = stateMachineMapper.selectById(orderId);
        if (entity != null && StringUtils.hasText(entity.getState())) {
            VoucherState state = VoucherState.valueOf(entity.getState());
            // 恢复状态机状态
            stateMachine.getStateMachineAccessor()
                    .doWithAllRegions(accessor -> accessor.resetStateMachine(new DefaultStateMachineContext<>(
                            state, null, null, null)));
        }
        return stateMachine;
    }
}

对应的状态守卫:

@Component
public class VoucherStateGuard implements Guard<VoucherState, VoucherEvent> {

    @Autowired
    private VoucherOrderMapper orderMapper;

    @Override
    public boolean evaluate(StateContext<VoucherState, VoucherEvent> context) {
        // 获取订单ID(从事件参数中传递)
        Long orderId = context.getMessage().getHeaders().get("orderId", Long.class);
        if (orderId == null) {
            context.getExtendedState().getVariables().put("error", "订单ID不能为空");
            return false;
        }

        // 校验订单是否存在
        VoucherOrder order = orderMapper.selectById(orderId);
        if (order == null) {
            context.getExtendedState().getVariables().put("error", "订单不存在");
            return false;
        }

        // 校验当前状态是否与流转源状态匹配
        VoucherState currentState = VoucherState.valueOf(order.getOrderState());
        VoucherState sourceState = context.getTransition().getSource().getId();
        if (!currentState.equals(sourceState)) {
            context.getExtendedState().getVariables().put("error", 
                String.format("当前状态为[%s],不支持该操作", currentState));
            return false;
        }

        return true;
    }
}

接着,在service中只需要对外提供一个服务,启用代码如下:

@Service
public class VoucherStateMachineService {

    @Autowired
    private StateMachineFactory<VoucherState, VoucherEvent> stateMachineFactory;
    @Autowired
    private StateMachinePersister<VoucherState, VoucherEvent, Long> persister;
    @Autowired
    private VoucherOrderMapper orderMapper;

    /**
     * 执行状态流转
     * @param orderId 订单ID
     * @param event 触发事件
     * @return 流转结果
     */
    public Result<String> executeEvent(Long orderId, VoucherEvent event) {
        try {
            // 1. 获取订单信息
            VoucherOrder order = orderMapper.selectById(orderId);
            if (order == null) {
                return Result.fail("订单不存在");
            }

            // 2. 创建状态机并恢复状态
            StateMachine<VoucherState, VoucherEvent> stateMachine = stateMachineFactory.getStateMachine();
            stateMachine.startReactively().block();
            persister.restore(stateMachine, orderId); // 从持久化存储恢复状态

            // 3. 发送事件(携带订单ID参数)
            Message<VoucherEvent> message = MessageBuilder
                .withPayload(event)
                .setHeader("orderId", orderId)
                .build();
            boolean result = stateMachine.sendEvent(message);

            // 4. 持久化状态机
            persister.persist(stateMachine, orderId);

            if (result) {
                return Result.success("状态流转成功");
            } else {
                // 获取守卫中的错误信息
                String error = stateMachine.getExtendedState().get("error", String.class);
                return Result.fail(error != null ? error : "状态流转失败");
            }
        } catch (Exception e) {
            return Result.fail("状态流转异常:" + e.getMessage());
        }
    }
}

效果测试

我们采用ApiFox插件测试接口:

image.png

带来的好处与问题是什么?

在面试中,如果你提出你使用过状态机,那么面试官就会问你,你使用状态机和使用普通接口流转带来的好处是什么?

好处如下:

  1. 使接口实现高内聚,无需引入额外其他接口带来额外线程开销
  2. 引入状态模式设计,代码清晰度更高,更易维护
  3. 状态事件更清晰,极大程度降低状态流转错乱带来的数据问题

好处说完了,说一下可能存在的问题:

  1. 分布式环境下,对同一订单操作可能存在状态越权的情况,例如多个实例操作同一订单,极有可能出现A将状态从草稿变成审核中,但是B将状态从审核中变成审核通过;并发操作带来的状态越权问题,解决方式:采用分布式锁或者乐观锁,在状态变更时加锁控制;
  2. 状态变成一个循环,比如从草稿变成待审核,又从待审核变成草稿;虽然业务上允许,但是这明显是两个事件触发,代码中需要注意这种循环状态不能由一个事件触发;