面向状态机编程:复杂业务逻辑应对之道
作者:京东零售 樊思国
一、背景
在研发项目中,经常能遇到复杂的状态流转类的业务场景,比如游戏编程中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); } }
五、总结
本文通过介绍有限状态机,并结合具体项目,通过状态机的应用将状态和业务逻辑解耦,便于简化复杂业务逻辑,降低理解成本。另外,状态机的应用场景对于那种复杂的流程也同样适合,可以在实际项目中根据状态机核心要素梳理出隐性的状态转换关系,从而将一个复杂的流程问题转换为状态机的模式问题,再利用状态机模式来实现,可以有助于我们优雅的解决更广泛的复杂业务问题。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
基于Istio的灰度发布架构方案实践之路
作者:京东物流 赵勇萍 1. 背景介绍 灰度发布,又名金丝雀发布,是指能够平滑过渡的一种发布方式。基于系统稳定性和快速业务迭代的综合考虑,业务应用开发团队采取了新版本服务灰度上线的方式,即新版本服务并非全量发布到线上环境,而是发布少数几个实例进行灰度验证,没有问题后再全量发布。在部分核心服务进行接口升级和逻辑迁移时,还会通过在业务逻辑代码中增加黑白名单或者流量百分比控制的方式,逐步将旧版本接口实现迁移至新版本接口实现。尤其是对于toB业务和SAAS类平台,很多情况需要根据租户或用户维度进行灰度控制,实现业务上的A/Best功能。 对于之前我们业务中实现的传统的灰度发布, 较好地权衡了服务稳定性和业务迭代效率,但仍存在以下问题: a. 系统侵入性强,业务开发人员在服务接口中编码大量与业务逻辑无关的黑白名单或流量百分比控制代码。 b. 当新版本接口出现波动或异常,通常需要业务团队通过紧急修改代码和紧急发布,来更新黑白名单和流量百分比控制策略,时间较长,业务影响较大。 c. 调整黑白名单或流量控制百分比,需要业务团队发布代码或者接入动态配置中心,过程复杂、可复用性差、操作灵活性差. d. 对...
- 下一篇
通用图像分割任务- 使用 Mask2Former 和 OneFormer
本文介绍两个领先的图像分割神经网络模型: Mask2Former 和 OneFormer。相关模型已经在 🤗 Transformers 提供。🤗 Transformers 是一个开源库,提供了很多便捷的先进模型。在本文中,你也会学到各种图像分割任务的不同之处。 图像分割 图像分割任务旨在鉴别区分出一张图片的不同部分,比如人物、汽车等等。从技术角度讲,图像分割任务需要根据不同的语义信息区分并聚集起对应相同语义的像素点。读者可以参考 Hugging Face 的 任务页面 来简要了解。 大体上,图像分割可以分为三个子任务: 实例分割 (instance segmentation) 、语义分割 (semantic segmentation) 、全景分割 (panoptic segmentation)。这三个子任务都有着大量的算法与模型。 实例分割 任务旨在区分不同的“实例”,例如图像中不同的人物个体。实例分割从某种角度看和物体检测很像,不同的是在这里我们需要的是一个对应类别的二元的分割掩膜,而不是一个检测框。实例也可以称为“物体 (objects)”或“实物 (things)”。需要注意...
相关文章
文章评论
共有0条评论来说两句吧...