作者:京东零售 樊思国
一、背景
在研发项目中,经常能遇到复杂的状态流转类的业务场景,比如游戏编程中NPC的跳跃、前进、转向等状态变化,电商领域订单的状态变化等。这类情况其实可以有一种优雅的实现方法:状态机。如下图,是操作系统对进程调度的状态机:
图 操作系统进程调度状态机
二、实现方式
面对以上场景,通常情况下的实现有以下几种,下面分别比较它们适用范围和优缺点:
2.1 if/else
优点:实现简单、直观。
缺点:状态多了代码可读性,业务与状态判断深度耦合,维护和扩展困难。
2.2 状态模式
状态模式类图及使用方式如下:
1public class StatePatternDemo { 2 public static void main(String[] args) { 3 Context context = new Context(); 4 5 StartState startState = new StartState(); 6 startState.doAction(context); 7 8 System.out.println(context.getState().toString()); 9 10 StopState stopState = new StopState(); 11 stopState.doAction(context); 12 13 System.out.println(context.getState().toString()); 14 } 15} 16 17
优点:状态单独实现,可读性比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 定义状态枚举
1public enum SkuStateEnum { 2 3 /** 4 * 未操作 5 */ 6 INIT(0, "未操作"), 7 /** 8 * 任务下发中 9 */ 10 TASK_DELIVERY(1, "任务下发中"), 11 /** 12 * 下发失败 13 */ 14 DELIVERY_FAIL(2, "下发失败"), 15 /** 16 * 复核中 17 */ 18 RECHECKING(3, "复核中"), 19 /** 20 * 已复核 21 */ 22 RECHECKED(4, "已复核"), 23 /** 24 * 自行联系供应商 25 */ 26 CONCAT_SUPPLIER(5, "自行联系供应商"), 27 /** 28 * 挂起 29 */ 30 SUSPEND(6, "挂起"); 31 32 /** 33 * 状态代码 34 */ 35 private Integer state; 36 /** 37 * 描述信息 38 */ 39 private String desc; 40 41 SkuStateEnum(Integer state, String desc) { 42 this.state = state; 43 this.desc = desc; 44 } 45 46 public static SkuStateEnum getByState(Integer state) { 47 for (SkuStateEnum skuStateEnum : SkuStateEnum.values()) { 48 if (skuStateEnum.getState().equals(state)) { 49 return skuStateEnum; 50 } 51 } 52 return null; 53 } 54 55 public Integer getState() { 56 return state; 57 } 58 59 public String getDesc() { 60 return desc; 61 } 62} 63 64
4.3.2 定义事件枚举
1public enum SkuAttrEventEnum { 2 /** 3 * 调用OMC属性采集接口成功 4 */ 5 INVOKE_OMC_ATTR_COLLECT_API_SUCCESS, 6 /** 7 * 调用OMC属性采集接口失败 8 */ 9 INVOKE_OMC_ATTR_COLLECT_API_FAIL, 10 /** 11 * 调用OMC下发查询接口并已经生成采集单 12 */ 13 INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH, 14 /** 15 * 调用OMC下发查询接口失败 16 */ 17 INVOKE_OMC_SKU_DELIVERY_API_FAIL, 18 /** 19 * OMC的MQ返回SKU属性已变更 20 */ 21 MQ_OMC_SKU_ATTR_CHANGED, 22 /** 23 * 调用商品中台jsf接口,返回SKU属性已变更 24 */ 25 INVOKE_SKU_ATTR_API_CHANGED, 26 /** 27 * 京东有库存 28 */ 29 HAS_JD_STOCK, 30 /** 31 * 京东无库存,VMI有库存 32 */ 33 NO_JD_STOCK_HAS_VMI_STOCK, 34 /** 35 * 京东和VMI均无库存 36 */ 37 NO_JD_STOCK_NO_VMI_STOCK, 38 /** 39 * 上传并复核 40 */ 41 UPLOAD_AND_RECHECK; 42} 43 44
4.3.3 配置状态机
1@Configuration 2@EnableStateMachineFactory 3@Slf4j 4public class SkuAttrStateMachineConfig extends StateMachineConfigurerAdapter<SkuStateEnum, SkuAttrEventEnum> { 5 6 /** 7 * 配置状态 8 * 9 * @param states 10 * @throws Exception 11 */ 12 @Override 13 public void configure(StateMachineStateConfigurer<SkuStateEnum, SkuAttrEventEnum> states) throws Exception { 14 states.withStates().initial(SkuStateEnum.INIT) 15 .states(EnumSet.allOf(SkuStateEnum.class)); 16 } 17 18 @Override 19 public void configure(StateMachineConfigurationConfigurer<SkuStateEnum, SkuAttrEventEnum> config) throws Exception { 20 config.withConfiguration().listener(listener()).autoStartup(false); 21 } 22 23 /** 24 * 配置状态转换和事件的关系 25 * 26 * @param transitions 27 * @throws Exception 28 */ 29 @Override 30 public void configure(StateMachineTransitionConfigurer<SkuStateEnum, SkuAttrEventEnum> transitions) 31 throws Exception { 32 transitions.withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.TASK_DELIVERY) 33 .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS) 34 .action(ctx -> { 35 log.info("[调用OMC属性采集接口成功],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(), 36 SkuStateEnum.TASK_DELIVERY.getDesc()); 37 }) 38 .and() 39 .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.DELIVERY_FAIL) 40 .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL) 41 .action(ctx -> { 42 log.info("[调用OMC属性采集接口失败],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(), 43 SkuStateEnum.DELIVERY_FAIL.getDesc()); 44 }) 45 .and() 46 .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.CONCAT_SUPPLIER) 47 .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK) 48 .action(ctx -> { 49 log.info("[京东无库存,VMI有库存],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(), 50 SkuStateEnum.CONCAT_SUPPLIER.getDesc()); 51 }) 52 .and() 53 .withExternal().source(SkuStateEnum.INIT).target(SkuStateEnum.SUSPEND) 54 .event(SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK) 55 .action(ctx -> { 56 log.info("[京东和VMI均无库存],状态变更:{} -> {}.", SkuStateEnum.INIT.getDesc(), 57 SkuStateEnum.SUSPEND.getDesc()); 58 }) 59 .and() 60 .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.TASK_DELIVERY) 61 .event(SkuAttrEventEnum.HAS_JD_STOCK) 62 .action(ctx -> { 63 log.info("[京东有库存],状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(), 64 SkuStateEnum.TASK_DELIVERY.getDesc()); 65 }) 66 .and() 67 .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.RECHECKING) 68 .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS) 69 .action(ctx -> { 70 log.info("[调用OMC属性采集接口成功],状态变更:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(), 71 SkuStateEnum.RECHECKING.getDesc()); 72 }) 73 .and() 74 .withExternal().source(SkuStateEnum.SUSPEND).target(SkuStateEnum.CONCAT_SUPPLIER) 75 .event(SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK) 76 .action(ctx -> { 77 log.info("[京东无库存,VMI有库存]:{} -> {}.", SkuStateEnum.SUSPEND.getDesc(), 78 SkuStateEnum.CONCAT_SUPPLIER.getDesc()); 79 }) 80 .and() 81 .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKING) 82 .event(SkuAttrEventEnum.INVOKE_OMC_SKU_DELIVERY_API_GATHER_FINISH) 83 .action(ctx -> { 84 log.info("[调用OMC下发查询接口并已经生成采集单]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(), 85 SkuStateEnum.RECHECKING.getDesc()); 86 }) 87 .and() 88 .withExternal().source(SkuStateEnum.TASK_DELIVERY).target(SkuStateEnum.RECHECKED) 89 .event(SkuAttrEventEnum.MQ_OMC_SKU_ATTR_CHANGED) 90 .action(ctx -> { 91 log.info("[OMC的MQ返回SKU属性已变更]:{} -> {}.", SkuStateEnum.TASK_DELIVERY.getDesc(), 92 SkuStateEnum.RECHECKED.getDesc()); 93 }) 94 .and() 95 .withExternal().source(SkuStateEnum.DELIVERY_FAIL).target(SkuStateEnum.TASK_DELIVERY) 96 .event(SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS) 97 .action(ctx -> { 98 log.info("[调用OMC属性采集接口成功]:{} -> {}.", SkuStateEnum.DELIVERY_FAIL.getDesc(), 99 SkuStateEnum.TASK_DELIVERY.getDesc()); 100 }) 101 .and() 102 .withExternal().source(SkuStateEnum.CONCAT_SUPPLIER).target(SkuStateEnum.RECHECKED) 103 .event(SkuAttrEventEnum.INVOKE_SKU_ATTR_API_CHANGED) 104 .action(ctx -> { 105 log.info("[调用商品中台jsf接口,返回SKU属性已变更]:{} -> {}.", SkuStateEnum.CONCAT_SUPPLIER.getDesc(), 106 SkuStateEnum.RECHECKED.getDesc()); 107 }); 108 } 109 110 /** 111 * 全局监听器 112 * 113 * @return 114 */ 115 private StateMachineListener<SkuStateEnum, SkuAttrEventEnum> listener() { 116 return new StateMachineListenerAdapter<SkuStateEnum, SkuAttrEventEnum>() { 117 @Override 118 public void transition(Transition<SkuStateEnum, SkuAttrEventEnum> transition) { 119 //当状态的转移在configure方法配置中时,会走到该方法。 120 log.info("[{}]状态变更:{} -> {}", transition.getKind().name(), 121 transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()), 122 transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId())); 123 } 124 125 @Override 126 public void eventNotAccepted(Message<SkuAttrEventEnum> event) { 127 //当发生的状态转移不在configure方法配置中时,会走到该方法,此处打印error日志,方便排查状态转移问题 128 log.error("事件未收到: {}", event); 129 } 130 131 private Object ofNullableState(SkuStateEnum s) { 132 return Optional.ofNullable(s) 133 .map(SkuStateEnum::getDesc) 134 .orElse(null); 135 } 136 }; 137 } 138} 139 140
4.4 业务逻辑处理
4.4.1 构建状态机
对每个sku的操作,通过状态机工厂stateMachineFactory.getStateMachine
1//注入状态机工厂实例 2@Autowired 3private StateMachineFactory<SkuStateEnum, SkuAttrEventEnum> stateMachineFactory; 4//构建状态机 5public StateMachine<SkuStateEnum, SkuAttrEventEnum> buildStateMachine(String skuId) throws BusinessException { 6 if (StringUtils.isEmpty(skuId)) { 7 return null; 8 } 9 StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = null; 10 try { 11 //从DB中获取当前skuId对应的状态 12 LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class); 13 query.eq(SkuAttrRecheckState::getSkuId, skuId); 14 SkuAttrRecheckState skuAttrRecheckState = this.baseMapper.selectOne(query); 15 SkuStateEnum skuStateEnum = SkuStateEnum.getByState( 16 skuAttrRecheckState == null ? SkuStateEnum.INIT.getState() : skuAttrRecheckState.getState()); 17 //从状态机工厂获取一个状态机 18 stateMachine = stateMachineFactory.getStateMachine(skuId); 19 stateMachine.stop(); 20 //配置状态机参数 21 stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> { 22 //配置状态机拦截器,当状态发生转移时,会走到该拦截器中 23 sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<SkuStateEnum, SkuAttrEventEnum>() { 24 @Override 25 public void preStateChange(State<SkuStateEnum, SkuAttrEventEnum> state, 26 Message<SkuAttrEventEnum> message, 27 Transition<SkuStateEnum, SkuAttrEventEnum> transition, 28 StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine, 29 StateMachine<SkuStateEnum, SkuAttrEventEnum> rootStateMachine) { 30 //获取状态转移时,对应的SKU详细信息 31 SkuAttrRecheckState result = JSON.parseObject( 32 String.class.cast(message.getHeaders().get(JSON_STR)), SkuAttrRecheckState.class); 33 //更新状态机转移后的状态(来自于4.3.3中的配置) 34 result.setState(state.getId().getState()); 35 //将状态机转移后的状态写入DB 36 LambdaQueryWrapper<SkuAttrRecheckState> query = Wrappers.lambdaQuery(SkuAttrRecheckState.class); 37 query.eq(SkuAttrRecheckState::getSkuId, result.getSkuId()); 38 if (baseMapper.exists(query)) { 39 UpdateWrapper<SkuAttrRecheckState> updateQuery = new UpdateWrapper<>(); 40 updateQuery.eq("sku_id",result.getSkuId()); 41 log.info("更新状态信息:{}", JSON.toJSONString(result)); 42 baseMapper.update(result, updateQuery); 43 } else { 44 log.info("写入状态信息:{}", JSON.toJSONString(result)); 45 baseMapper.insert(result); 46 } 47 } 48 }); 49 //将状态机的初始状态配置为DB中的skuId对应状态 50 sma.resetStateMachine(new DefaultStateMachineContext<SkuStateEnum, SkuAttrEventEnum>( 51 skuStateEnum, null, null, null)); 52 }); 53 //启动状态机 54 stateMachine.start(); 55 } catch (Exception e) { 56 log.error("skuId={},构建状态机失败.", skuId, e); 57 throw new BusinessException("状态机构建失败", e); 58 } 59 return stateMachine; 60 } 61 62
4.4.2 封装事件
1public synchronized Boolean sendEvent(StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine, 2 SkuAttrEventEnum skuAttrEventEnum, SkuAttrRecheckState skuAttrRecheckState) throws BusinessException { 3 try { 4 //发送事件,并将需要传递的信息写入header 5 stateMachine.sendEvent(MessageBuilder.withPayload(skuAttrEventEnum) 6 .setHeader(SKU_ID, skuAttrRecheckState.getSkuId()) 7 .setHeader(STATE, skuAttrRecheckState.getState()) 8 .setHeader(JSON_STR, JSON.toJSONString(skuAttrRecheckState)) 9 .build()); 10 } catch (Exception e) { 11 log.error("发送事件失败", e); 12 throw new BusinessException("发送事件失败", e); 13 } 14 return true; 15 } 16 17
4.4.3 业务逻辑应用
当用户在界面上对“未操作”状态的SKU点击审核按钮时,会调用物流OMC接口将SKU属性下发到物流侧,当下发成功时,状态会转换为“任务下发中”,当调用接口失败,则会将状态转换为"下发失败",核心代码如下:
1public Boolean recheck(List<String> skuIds) throws BusinessException { 2 if (CollectionUtils.isEmpty(skuIds)) { 3 log.error("参数错误,sku列表为空"); 4 throw new BusinessException("参数错误,sku列表为空"); 5 } 6 List<SkuAttrExceptionDetail> skuDetails = skuAttrExceptionDetailMapper.queryBySkuIdList(skuIds); 7 if (CollectionUtils.isEmpty(skuDetails)) { 8 log.error("查询sku异常明细结果集为空,skuIds={}", JSON.toJSONString(skuIds)); 9 return false; 10 } 11 for (SkuAttrExceptionDetail detail : skuDetails) { 12 if (detail.getState() != SkuStateEnum.INIT.getState()) { 13 log.info("{}不是未操作状态sku不进行复核", detail.getSkuId()); 14 continue; 15 } 16 //构建SKU对应的状态机 17 StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine = buildStateMachine(detail.getSkuId()); 18 SkuAttrRecheckState skuAttrRecheckState = DomainBuilderUtil.buildSkuAttrRecheckState(detail); 19 //判定库存并发送事件 20 adjustAndSendEvents(detail, stateMachine, skuAttrRecheckState); 21 } 22 return true; 23 } 24 25public void adjustAndSendEvents(SkuAttrExceptionDetail detail, 26 StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine, 27 SkuAttrRecheckState skuAttrRecheckState) throws BusinessException { 28 //1、京东有库存,调用物流属性接口,下发SKU属性 29 if (detail.getSpotInventoryQtty() > 0) { 30 invokeOmcSkuAttrCollectApiAndSendEvent(detail, stateMachine, skuAttrRecheckState); 31 return; 32 } 33 //2、京东无库存,有VMI库存 34 if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() > 0) { 35 sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_HAS_VMI_STOCK, skuAttrRecheckState); 36 return; 37 } 38 //3、京东和VMI均无库存 39 if (detail.getSpotInventoryQtty() <= 0 && detail.getVmiInventoryQtty() <= 0) { 40 sendEvent(stateMachine, SkuAttrEventEnum.NO_JD_STOCK_NO_VMI_STOCK, skuAttrRecheckState); 41 return; 42 } 43 } 44 45private void invokeOmcSkuAttrCollectApiAndSendEvent(SkuAttrExceptionDetail detail, 46 StateMachine<SkuStateEnum, SkuAttrEventEnum> stateMachine, 47 SkuAttrRecheckState skuAttrRecheckState) throws BusinessException { 48 DistrustAttributeGatherRequest request = RequestUtil.buildOmcAttrCollectRequest(detail, reqSource); 49 try { 50 if (jsfInvokeService.invokeSkuAttrCollectApi(request)) { 51 sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_SUCCESS, 52 skuAttrRecheckState); 53 } else { 54 sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState); 55 } 56 } catch (Exception e) { 57 log.error("调用物流Sku属性采集接口错误,request={}", JSON.toJSONString(request), e); 58 sendEvent(stateMachine, SkuAttrEventEnum.INVOKE_OMC_ATTR_COLLECT_API_FAIL, skuAttrRecheckState); 59 } 60 } 61 62
五、总结
本文通过介绍有限状态机,并结合具体项目,通过状态机的应用将状态和业务逻辑解耦,便于简化复杂业务逻辑,降低理解成本。另外,状态机的应用场景对于那种复杂的流程也同样适合,可以在实际项目中根据状态机核心要素梳理出隐性的状态转换关系,从而将一个复杂的流程问题转换为状态机的模式问题,再利用状态机模式来实现,可以有助于我们优雅的解决更广泛的复杂业务问题。
