简介: 订单状态流转是交易系统的最为核心的工作,订单系统往往都会存在状态多、链路长、逻辑复杂的特点,还存在多场景、多类型、多业务维度等业务特性。在保证订单状态流转稳定性的前提下、可扩展性和可维护性是我们需要重点关注和解决的问题。
作者 | 亮言
来源 | 阿里技术公众号
一 背景
订单状态流转是交易系统的最为核心的工作,订单系统往往都会存在状态多、链路长、逻辑复杂的特点,还存在多场景、多类型、多业务维度等业务特性。在保证订单状态流转稳定性的前提下、可扩展性和可维护性是我们需要重点关注和解决的问题。
以高德打车业务的订单状态为例,订单状态就有乘客下单、司机接单、司机已到达乘车点、开始行程、行程结束、确认费用、支付成功、订单取消、订单关闭等;订单车型有专车、快车、出租车等几种车型,而专车又分舒适型、豪华型、商务型等;业务场景接送机、企业用车、城际拼车等等场景。
当订单状态、类型、场景、以及其他一些维度组合时,每一种组合都可能会有不同的处理逻辑、也可能会存在共性的业务逻辑,这种情况下代码中各种if-else肯定是不敢想象的。怎么处理这种"多状态+多类型+多场景+多维度"的复杂订单状态流转业务,又要保证整个系统的可扩展性和可维护性,本文的解决思路和方案同大家一起探讨。
二 实现方案
要解决"多状态+多类型+多场景+多维度"的复杂订单状态流转业务,我们从纵向和横向两个维度进行设计。纵向主要从业务隔离和流程编排的角度出发解决问题、而横向主要从逻辑复用和业务扩展的角度解决问题。
1 纵向解决业务隔离和流程编排
状态模式的应用
通常我们处理一个多状态或者多维度的业务逻辑,都会采用状态模式或者策略模式来解决,我们这里不讨论两种设计模式的异同,其核心其实可以概括为一个词"分而治之",抽象一个基础逻辑接口、每一个状态或者类型都实现该接口,业务处理时根据不同的状态或者类型调用对应的业务实现,以到达逻辑相互独立互不干扰、代码隔离的目的。
这不仅仅是从可扩展性和可维护性的角度出发,其实我们做架构做稳定性、隔离是一种减少影响面的基本手段,类似的隔离环境做灰度、分批发布等,这里不做扩展。
/**
* 状态机处理器接口
*/
public interface StateProcessor {
/**
* 执行状态迁移的入口
*/
void action(StateContext context) throws Exception;
}
/**
* 状态A对应的状态处理器
*/
public class StateAProcessor interface StateProcessor {
/**
* 执行状态迁移的入口
*/
@Override
public void action(StateContext context) throws Exception {
}
}
单一状态或类型可以通过上面的方法解决,那么"多状态+多类型+多场景+多维度"这种组合业务呢,当然也可以采用这种模式或思路来解决。首先在开发阶段通过一个注解@OrderPorcessor将不同的维度予以组合、开发出多个对应的具体实现类,在系统运行阶段,通过判断上下文来动态选择具体使用哪一个实现类执行。@OrderPorcessor中分别定义state代表当前处理器要处理的状态,bizCode和sceneId分别代表业务类型和场景,这两个字段留给业务进行扩展,比如可以用bizCode代表产品或订单类型、sceneId代表业务形态或来源场景等等,如果要扩展多个维度的组合、也可以用多个维度拼接后的字符串赋值到bizCode和sceneId上。
受限于Java枚举不能继承的规范,如果要开发通用的功能、注解中就不能使用枚举、所以此处只好使用String。
/**
* 状态机引擎的处理器注解标识
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Component
public @interface OrderProcessor {
/**
* 指定状态,state不能同时存在
*/
String[] state() default {};
/**
* 业务
*/
String[] bizCode() default {};
/**
* 场景
*/
String[] sceneId() default {};
}
/**
* 创建订单状态对应的状态处理器
*/
@OrderProcessor(state = "INIT", bizCode = {"CHEAP","POPULAR"}, sceneId = "H5")
public class StateCreateProcessor interface StateProcessor {
}
再想一下,因为涉及到状态流转,不可能会是一个状态A只能流转到状态B、状态A可能在不同的场景下流转到状态B、状态C、状态D;还有虽然都是由状态A流转到状态B、但是不同的场景处理流程也可能不一样,比如都是将订单从从待支付状态进行支付、用户主动发起支付和系统免密支付的流程可能就不一样。针对上面这两种情况、我们把这里的"场景"统一封装为"事件(event)",以"事件驱动"的方式来控制状态的流向,一个状态遇到一个特定的处理事件来决定该状态的业务处理流程和最终状态流向。我们可以总结下,其实状态机模式简单说就是:基于某些特定业务和场景下,根据源状态和发生的事件,来执行下一步的流程处理逻辑,并设置一个目标状态。
这里有人可能有一些疑问,这个"事件"和上面说的"多场景"、"多维度"有什么不一样。解释一下,我们这里说的是"事件"是一个具体的业务要执行的动作,比如用户下单是一个业务事件、用户取消订单是一个业务事件、用户支付订单也是一个业务事件。而"多场景"、"多维度"则是可交由业务自行进行扩展的维度,比如自有标准模式来源的订单、通过开放平台API来的订单、通过第三方标准来源的订单,某某小程序、某某APP来源可以定义为不同场景,而接送机、企业用车、拼车等可以定义为维度。
public @interface OrderProcessor {
/**
* 指定状态
*/
String[] state() default {};
/**
* 订单操作事件
*/
String event();
......
}
/**
* 订单状态迁移事件
*/
public interface OrderStateEvent {
/**
* 订单状态事件
*/
String getEventType();
/**
* 订单ID
*/
String getOrderId();
/**
* 如果orderState不为空,则代表只有订单是当前状态才进行迁移
*/
default String orderState() {
return null;
}
/**
* 是否要新创建订单
*/
boolean newCreate();
}
状态迁移流程的封装
在满足了上面说的多维度组合的业务场景、开发多个实现类来执行的情况,我们思考执行这些实现类在流程上是否有再次抽象和封装的地方、以减少研发工作量和尽量的实现通用流程。我们经过观察和抽象,发现每一个订单状态流转的流程中,都会有三个流程:校验、业务逻辑执行、数据更新持久化;于是再次抽象,可以将一个状态流转分为数据准备(prepare)——>校验(check)——>获取下一个状态(getNextState)——>业务逻辑执行(action)——>数据持久化(save)——>后续处理(after)这六个阶段;然后通过一个模板方法将六个阶段方法串联在一起、形成一个有顺序的执行逻辑。这样一来整个状态流程的执行逻辑就更加清晰和简单了、可维护性上也得到的一定的提升。
/**
* 状态迁移动作处理步骤
*/
public interface StateActionStep<T, C> {
/**
* 准备数据
*/
default void prepare(StateContext<C> context) {
}
/**
* 校验
*/
ServiceResult<T> check(StateContext<C> context);
/**
* 获取当前状态处理器处理完毕后,所处于的下一个状态
*/
String getNextState(StateContext<C> context);
/**
* 状态动作方法,主要状态迁移逻辑
*/
ServiceResult<T> action(String nextState, StateContext<C> context) throws Exception;
/**
* 状态数据持久化
*/
ServiceResult<T> save(String nextState, StateContext<C> context) throws Exception;
/**
* 状态迁移成功,持久化后执行的后续处理
*/
void after(StateContext<C> context);
}
/**
* 状态机处理器模板类
*/
@Component
public abstract class AbstractStateProcessor<T, C> implements StateProcessor<T, C>, StateActionStep<T, C> {
@Override
public final ServiceResult<T> action(StateContext<C> context) throws Exception {
ServiceResult<T> result = null;
try {
// 数据准备
this.prepare(context);
// 串行校验器
result = this.check(context);
if (!result.isSuccess()) {
return result;
}
// getNextState不能在prepare前,因为有的nextState是根据prepare中的数据转换而来
String nextState = this.getNextState(context);
// 业务逻辑
result = this.action(nextState, context);
if (!result.isSuccess()) {
return result;
}
// 持久化
result &#