乐趣区

关于编程:面向状态机编程复杂业务逻辑应对之道

作者:京东批发 樊思国

一、背景

在研发我的项目中,常常能遇到简单的状态流转类的业务场景,比方游戏编程中 NPC 的跳跃、后退、转向等状态变动,电商畛域订单的状态变动等。这类状况其实能够有一种优雅的实现办法:状态机。如下图,是操作系统对过程调度的状态机:

图 操作系统过程调度状态机

二、实现形式

面对以上场景,通常状况下的实现有以下几种,上面别离比拟它们适用范围和优缺点:

2.1 if/else

长处:实现简略、直观。

毛病:状态多了代码可读性,业务与状态判断深度耦合,保护和扩大艰难。

2.2 状态模式

状态模式类图及应用形式如下:

public class StatePatternDemo {public static void main(String[] args) {Context context = new Context();
 
      StartState startState = new StartState();
      startState.doAction(context);
 
      System.out.println(context.getState().toString());
 
      StopState stopState = new StopState();
      stopState.doAction(context);
 
      System.out.println(context.getState().toString());
   }
}

长处:状态独自实现,可读性比 if/else 好。

毛病:扩大状态需减少状态类,状态多了会呈现很多状态类;并没有齐全实现状态与业务解耦,不利于保护和理解整个零碎状态全貌。

2.3 无限状态机

长处:谨严的数学模型,状态转移和业务逻辑基于事件齐全解耦,能看到整个零碎状态全貌便于保护和扩大。

毛病:需引入状态机实现形式,具备肯定了解老本。

三、无限状态机

3.1 定义

无限状态机(英语:finite-state machine,缩写:FSM)又称无限状态自动机(英语:finite-state automaton,缩写:FSA),简称状态机,是示意无限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

3.2 要害概念

  • 状态 State:个别在状态转移图中用圆圈示意。
  • 事件 Event:示意从一种状态迁徙到另一种状态的触发机制。对应状态转换图中的箭头局部。
  • 动作 Action: 示意状态转换后,执行的动作,但不是必须的,也能够状态转换后不执行任何动作。
  • 转移 Transition:示意状态转换,从原始状态迁徙到目标状态的一个过程。
  • 条件 Guard:示意产生状态转移需满足的条件。

3.3 技术选型

在 Java 我的项目中,比拟罕用的有 Spring Statemachine 和 Squirrel-foundation。

框架 长处 毛病
Spring Statemachine 基于 Spring 生态,社区弱小。性能齐备,反对多种状态机配置和长久化形式。 较为重量级,额定性能多。单例模式状态机不保障线程平安,只能通过工厂模式创立新的状态机实例实现,对性能有肯定影响。
Squirrel-foundation 轻量级实现,状态机的创立开销小。便于二次革新,实现定制业务。 社区没有 spring 沉闷。非凡约定较多。

综上,在上面的我的项目中,因为团队应用 SpringBoot 作为开发框架,并且我的项目不波及高并发场景,故抉择 Spring Statemachine。

四、我的项目实战

在事实我的项目中,碰到多种状态转换的简单业务流程,能够通过以下几个步骤进行分级,逐渐将产品需要清晰的实现进去:

4.1 需要背景

批发采销在保护 SKU 物流属性(长、宽、高和分量)时,会因为和物流仓库侧理论属性存在不统一的状况,导致带来物流老本的偏差。为解决这个问题,需设计一个零碎供采销通过操作 SKU 的形式,将属性下发给物流侧审核,失去最终的精确物流属性。在对 SKU 进行审核操作的过程中,别离存在未操作、工作下发中、下发失败、已审核、自行分割供应商和挂起 6 种状态(状态转换详见 4.2),思考到这些状态转换的条件散布在不同的场景下,处于对可维护性和扩展性的思考,采纳状态机实现该需要。

4.2 状态转换图

通过梳理状态转换关系,画出状态转换图如下:

SKU 属性审核状态转换图

4.3 配置状态机

4.3.1 定义状态枚举

public enum SkuStateEnum {

    /**
     * 未操作
     */
    INIT(0, "未操作"),
    /**
     * 工作下发中
     */
    TASK_DELIVERY(1, "工作下发中"),
    /**
     * 下发失败
     */
    DELIVERY_FAIL(2, "下发失败"),
    /**
     * 复核中
     */
    RECHECKING(3, "复核中"),
    /**
     * 已复核
     */
    RECHECKED(4, "已复核"),
    /**
     * 自行分割供应商
     */
    CONCAT_SUPPLIER(5, "自行分割供应商"),
    /**
     * 挂起
     */
    SUSPEND(6, "挂起");

    /**
     * 状态代码
     */
    private Integer state;
    /**
     * 形容信息
     */
    private String desc;

    SkuStateEnum(Integer state, String desc) {
        this.state = state;
        this.desc = desc;
    }

    public static SkuStateEnum getByState(Integer state) {for (SkuStateEnum skuStateEnum : SkuStateEnum.values()) {if (skuStateEnum.getState().equals(state)) {return skuStateEnum;}
        }
        return null;
    }

    public Integer getState() {return state;}

    public String getDesc() {return desc;}
}

4.3.2 定义事件枚举

public enum SkuAttrEventEnum {
    /**
     * 调用 OMC 属性采集接口胜利
     */
    INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,
    /**
     * 调用 OMC 属性采集接口失败
     */
    INVOKE_OMC_ATTR_COLLECT_API_FAIL,
    /**
     * 调用 OMC 下发查问接口并曾经生成采集单
     */
    INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH,
    /**
     * 调用 OMC 下发查问接口失败
     */
    INVOKE_OMC_SKU_DELIVERY_API_FAIL,
    /**
     * OMC 的 MQ 返回 SKU 属性已变更
     */
    MQ_OMC_SKU_ATTR_CHANGED,
    /**
     * 调用商品中台 jsf 接口,返回 SKU 属性已变更
     */
    INVOKE_SKU_ATTR_API_CHANGED,
    /**
     * 京东有库存
     */
    HAS_JD_STOCK,
    /**
     * 京东无库存,VMI 有库存
     */
    NO_JD_STOCK_HAS_VMI_STOCK,
    /**
     * 京东和 VMI 均无库存
     */
    NO_JD_STOCK_NO_VMI_STOCK,
    /**
     * 上传并复核
     */
    UPLOAD_AND_RECHECK;
}

4.3.3 配置状态机

@Configuration
@EnableStateMachineFactory
@Slf4j
public class SkuAttrStateMachineConfig extends StateMachineConfigurerAdapter<SkuStateEnum, SkuAttrEventEnum> {

    /**
     * 配置状态
     *
     * @param states
     * @throws Exception
     */
    @Override
    public void configure(StateMachineStateConfigurer<SkuStateEnum, SkuAttrEventEnum> states) throws Exception {states.withStates().initial(SkuStateEnum.INIT)
            .states(EnumSet.allOf(SkuStateEnum.class));
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<SkuStateEnum, SkuAttrEventEnum> config) throws Exception {config.withConfiguration().listener(listener()).autoStartup(false);
    }

    /**
     * 配置状态转换和事件的关系
     *
     * @param transitions
     * @throws Exception
     */
    @Override
    public void configure(StateMachineTransitionConfigurer<SkuStateEnum, SkuAttrEventEnum> transitions)
        throws Exception {transitions.withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {log.info("[ 调用 OMC 属性采集接口胜利], 状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.DELIVERY_FAIL)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL)
            .action(ctx -> {log.info("[ 调用 OMC 属性采集接口失败], 状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.DELIVERY_FAIL.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.CONCAT_SUPPLIER)
            .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK)
            .action(ctx -> {log.info("[ 京东无库存,VMI 有库存], 状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.CONCAT_SUPPLIER.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.SUSPEND)
            .event(SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK)
            .action(ctx -> {log.info("[ 京东和 VMI 均无库存], 状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(),
                    SkuStateEnum.SUSPEND.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.HAS_JD_STOCK)
            .action(ctx -> {log.info("[ 京东有库存], 状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.RECHECKING)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {log.info("[ 调用 OMC 属性采集接口胜利], 状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.RECHECKING.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.CONCAT_SUPPLIER)
            .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK)
            .action(ctx -> {log.info("[ 京东无库存,VMI 有库存]:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(),
                    SkuStateEnum.CONCAT_SUPPLIER.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKING)
            .event(SkuAttrEventEnum.INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH)
            .action(ctx -> {log.info("[ 调用 OMC 下发查问接口并曾经生成采集单]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),
                    SkuStateEnum.RECHECKING.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKED)
            .event(SkuAttrEventEnum.MQ_OMC_SKU_ATTR_CHANGED)
            .action(ctx -> {log.info("[OMC 的 MQ 返回 SKU 属性已变更]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(),
                    SkuStateEnum.RECHECKED.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.DELIVERY_FAIL).target(SkuStateEnum.TASK_DELIVERY)
            .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS)
            .action(ctx -> {log.info("[ 调用 OMC 属性采集接口胜利]:{} -> {}.", SkuStateEnum.DELIVERY_FAIL.getDesc(),
                    SkuStateEnum.TASK_DELIVERY.getDesc());
            })
            .and()
            .withExternal().source(SkuStateEnum.CONCAT_SUPPLIER).target(SkuStateEnum.RECHECKED)
            .event(SkuAttrEventEnum.INVOKE_SKU_ATTR_API_CHANGED)
            .action(ctx -> {log.info("[ 调用商品中台 jsf 接口,返回 SKU 属性已变更]:{} -> {}.", SkuStateEnum.CONCAT_SUPPLIER.getDesc(),
                    SkuStateEnum.RECHECKED.getDesc());
            });
    }

    /**
     * 全局监听器
     *
     * @return
     */
    private StateMachineListener<SkuStateEnum, SkuAttrEventEnum> listener() {return new StateMachineListenerAdapter<SkuStateEnum, SkuAttrEventEnum>() {
            @Override
            public void transition(Transition<SkuStateEnum, SkuAttrEventEnum> transition) {
                // 当状态的转移在 configure 办法配置中时,会走到该办法。log.info("[{}] 状态变更:{} -> {}", transition.getKind().name(),
                    transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()),
                    transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId()));
            }

            @Override
            public void eventNotAccepted(Message<SkuAttrEventEnum> event) {
                // 当产生的状态转移不在 configure 办法配置中时,会走到该办法,此处打印 error 日志,不便排查状态转移问题
                log.error("事件未收到: {}", event);
            }

            private Object ofNullableState(SkuStateEnum s) {return Optional.ofNullable(s)
                    .map(SkuStateEnum::getDesc)
                    .orElse(null);
            }
        };
    }
}

4.4 业务逻辑解决

4.4.1 构建状态机

对每个 sku 的操作,通过状态机工厂 stateMachineFactory.getStateMachine

// 注入状态机工厂实例
@Autowired
private StateMachineFactory<SkuStateEnum, SkuAttrEventEnum> stateMachineFactory;
// 构建状态机
public StateMachine<SkuStateEnum, SkuAttrEventEnum> buildStateMachine(String skuId) throws BusinessException {if (StringUtils.isEmpty(skuId)) {return null;}
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = null;
        try {
            // 从 DB 中获取以后 skuId 对应的状态
            LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);
            query.eq(SkuAttrRecheckState::getSkuId, skuId);
            SkuAttrRecheckState skuAttrRecheckState = this.baseMapper.selectOne(query);
            SkuStateEnum skuStateEnum = SkuStateEnum.getByState(skuAttrRecheckState == null ? SkuStateEnum.INIT.getState() : skuAttrRecheckState.getState());
            // 从状态机工厂获取一个状态机
            stateMachine = stateMachineFactory.getStateMachine(skuId);
            stateMachine.stop();
            // 配置状态机参数
            stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> {
                // 配置状态机拦截器,当状态产生转移时,会走到该拦截器中
                sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<SkuStateEnum, SkuAttrEventEnum>() {
                    @Override
                    public void preStateChange(State<SkuStateEnum, SkuAttrEventEnum> state,
                        Message<SkuAttrEventEnum> message,
                        Transition<SkuStateEnum, SkuAttrEventEnum> transition,
                        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
                        StateMachine<SkuStateEnum, SkuAttrEventEnum> rootStateMachine) {
                        // 获取状态转移时,对应的 SKU 详细信息
                        SkuAttrRecheckState result = JSON.parseObject(String.class.cast(message.getHeaders().get(JSON_STR)), SkuAttrRecheckState.class);
                        // 更新状态机转移后的状态(来自于 4.3.3 中的配置)result.setState(state.getId().getState());
                        // 将状态机转移后的状态写入 DB
                        LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class);
                        query.eq(SkuAttrRecheckState::getSkuId, result.getSkuId());
                        if (baseMapper.exists(query)) {UpdateWrapper<SkuAttrRecheckState> updateQuery = new UpdateWrapper<>();
                            updateQuery.eq("sku_id",result.getSkuId());
                            log.info("更新状态信息:{}", JSON.toJSONString(result));
                            baseMapper.update(result, updateQuery);
                        } else {log.info("写入状态信息:{}", JSON.toJSONString(result));
                            baseMapper.insert(result);
                        }
                    }
                });
                // 将状态机的初始状态配置为 DB 中的 skuId 对应状态
                sma.resetStateMachine(new DefaultStateMachineContext<SkuStateEnum, SkuAttrEventEnum>(skuStateEnum, null, null, null));
            });
            // 启动状态机
            stateMachine.start();} catch (Exception e) {log.error("skuId={},构建状态机失败.", skuId, e);
            throw new BusinessException("状态机构建失败", e);
        }
        return stateMachine;
    }

4.4.2 封装事件

public synchronized Boolean sendEvent(StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrEventEnum skuAttrEventEnum, SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {
        try {
            // 发送事件,并将须要传递的信息写入 header
            stateMachine.sendEvent(MessageBuilder.withPayload(skuAttrEventEnum)
                .setHeader(SKU_ID, skuAttrRecheckState.getSkuId())
                .setHeader(STATE, skuAttrRecheckState.getState())
                .setHeader(JSON_STR, JSON.toJSONString(skuAttrRecheckState))
                .build());
        } catch (Exception e) {log.error("发送事件失败", e);
            throw new BusinessException("发送事件失败", e);
        }
        return true;
    }

4.4.3 业务逻辑利用

当用户在界面上对“未操作”状态的 SKU 点击审核按钮时,会调用物流 OMC 接口将 SKU 属性下发到物流侧,当下发胜利时,状态会转换为“工作下发中”,当调用接口失败,则会将状态转换为 ” 下发失败 ”,外围代码如下:

public Boolean recheck(List<String> skuIds) throws BusinessException {if (CollectionUtils.isEmpty(skuIds)) {log.error("参数谬误,sku 列表为空");
            throw new BusinessException("参数谬误,sku 列表为空");
        }
        List<SkuAttrExceptionDetail> skuDetails = skuAttrExceptionDetailMapper.queryBySkuIdList(skuIds);
        if (CollectionUtils.isEmpty(skuDetails)) {log.error("查问 sku 异样明细后果集为空,skuIds={}", JSON.toJSONString(skuIds));
            return false;
        }
        for (SkuAttrExceptionDetail detail : skuDetails) {if (detail.getState() != SkuStateEnum.INIT.getState()) {log.info("{} 不是未操作状态 sku 不进行复核", detail.getSkuId());
                continue;
            }
            // 构建 SKU 对应的状态机
            StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = buildStateMachine(detail.getSkuId());
            SkuAttrRecheckState skuAttrRecheckState = DomainBuilderUtil.buildSkuAttrRecheckState(detail);
            // 断定库存并发送事件
            adjustAndSendEvents(detail, stateMachine, skuAttrRecheckState);
        }
        return true;
    }

public void adjustAndSendEvents(SkuAttrExceptionDetail detail,
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {
        //1、京东有库存,调用物流属性接口,下发 SKU 属性
        if (detail.getSpotInventoryQtty() > 0) {invokeOmcSkuAttrCollectApiAndSendEvent(detail, stateMachine, skuAttrRecheckState);
            return;
        }
        //2、京东无库存,有 VMI 库存
        if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() > 0) {sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK, skuAttrRecheckState);
            return;
        }
        //3、京东和 VMI 均无库存
        if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() <= 0) {sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK, skuAttrRecheckState);
            return;
        }
    }

private void invokeOmcSkuAttrCollectApiAndSendEvent(SkuAttrExceptionDetail detail,
        StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine,
        SkuAttrRecheckState skuAttrRecheckState) throws BusinessException {DistrustAttributeGatherRequest request = RequestUtil.buildOmcAttrCollectRequest(detail, reqSource);
        try {if (jsfInvokeService.invokeSkuAttrCollectApi(request)) {
                sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS,
                    skuAttrRecheckState);
            } else {sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);
            }
        } catch (Exception e) {log.error("调用物流 Sku 属性采集接口谬误,request={}", JSON.toJSONString(request), e);
            sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState);
        }
    }

五、总结

本文通过介绍无限状态机,并联合具体我的项目,通过状态机的利用将状态和业务逻辑解耦,便于简化简单业务逻辑,升高了解老本。另外,状态机的利用场景对于那种简单的流程也同样适宜,能够在理论我的项目中依据状态机外围因素梳理出隐性的状态转换关系,从而将一个简单的流程问题转换为状态机的模式问题,再利用状态机模式来实现,能够有助于咱们优雅的解决更宽泛的简单业务问题。

退出移动版