用有限状态机验证工作流设计

工作流编排

在电子商务领域,一个订单的生命周期,往往长达数天,涉及到创建,支付,履约等行为和状态。怎么管理订单状态呢?如果任何一个REST API, 都能够随意修改订单状态,那么订单状态将漏洞百出,容易有资金损失风险。 这个时候,就需要诸如Camunda, AWS Step Functions Workflow Studio之类的工作流引擎Workflow Engine 编排工具,来配置状态转移的时序逻辑,前提条件,和允许范围等。 工作流编排,适合于连接和管理松散的用户行为,逐渐流行,不限于电商领域。但是,工作流仍然有随意性。这篇文章将解释,并推荐解决办法。

电商订单有限状态机

举一个最简单的订单有限状态机:虚拟商品。比如说,购买桌面应用的license。其分成如下四个状态:initiated, paid, delivered, canceled。理所当然的,也有四个对应的动作。

输入图片说明

每个用户动作,都有一个WorkflowHandler来处理。WorkflowHandler类里面,一般要做如下工作。

  • 用户动作,诸如支付,履约,通过HTTP请求发送过来。

  • 根据规则,比如validate()方法,找到对应的 WorkflowHandler。

  • 调用run()方法,来处理用户动作。

  • 然后等待下一个用户动作。

说明:本文采用无状态设计。但是生产中,每一个workflow instance在创建的时候,都有一个instance_id, 以及目前流转到的工作流的节点,来保证工作流只能在这个有向图中,按照规定转换节点。

以下是对PayHandler,和DeliverHandler的Java代码实现。

class PayHandler extends WorkflowHandler {
  
  @Override
  public boolean validate(Context context) {
    return OrderState.INITIATED == orderEventService.getLatestState(context);
      && OrderAction.PAY == context.get("orderAction");
  }
  
  @Override
  public void run(Context context) {
    payService.exec(context.get("paymentInfo"));
    OrderEvent orverEvent = orderEventService.create(context, OrderState.PAID);
  }
}
class DeliverHandler extends WorkflowHandler {
  
  @Override
  public boolean validate(Context context) {
    return OrderState.PAID == orderEventService.getLatestState(context);
      && OrderAction.DELIVER == context.get("orderAction");
  }
  
  @Override
  public void run(Context context) {
    deliverService.exec(context.get("deliveryInfo"));
    OrderEvent orverEvent = orderEventService.create(context, OrderState.DELIVERED);
  }
}

上述两WorkflowHandler都有调用OrderEventService,来间接读写数据库。

class OrderEventService {
  
  public OrderState getLatestState(Context context) {
    List<OrderEvent> orderEvents = orderEventRepo.getByOrderId(context);
    return orderEvents.getLast().getState(); // if present
  }
​
  public void create(Context context, OrderState orderState) {
    orderEventRepo.create(context, orderState);
  }  
}

工作流的局限

举个例子,如果业务和开发都比较粗心,没有完全理解订单状态机,结果设计出来的工作流违背了状态机的要求。比如说,业务要求,有一批license, 要绕开支付这个操作,直接从initiated状态,跳到delivered状态,通过履约的方式销毁。如下

输入图片说明

其中,uncompliant 是强行加入的不合规步骤。其代码实现如下。

class UncompliantHandler extends WorkflowHandler {
​
  @Override
  public boolean validate(Context context) {
    return OrderState.INITIATED == orderEventService.getLatestState(context);
      && OrderAction.PAY == context.get("orderAction");
  }
​
  @Override
  public void run(Context context) {
    deliverService.exec(context.get("deliveryInfo"));
    orderEventService.create(context, OrderState.DELIVERED);
  }
}

如果没有一个强有力的保障,可能上述不合规的代码就直接发布到生产环境了,直到产生了真实数据,数据分析团队监控到了,才开始报警。

解决方案:状态机校验

显然,即使工作流图 和状态机 都是有向图,但是两者层级不同。但是工作流是能为了业务目标,修改拓扑结构的。状态机是对底层物理实现的反应,拓扑结构不能修改。那么,可以通过校验状态机的方式,检查工作流设计是否合理。代码示例如下:

class OrderEventService {
​
  public void create(Context context, OrderState nextState) {
    OrderState latestState = getLatestState(context);
    // Verify the state machine
    OrderEventValidator.validate(latestState, nextState);
    orderEventRepo.create(context, nextState);
  }
}
​
class OrderEventValidator {
  
  // state machine rule
  final private static Map<OrderState, Set<OrderState>> nextStateMap = Map.of(
    OrderState.INITIATED, Set.of(OrderState.PAID, OrderState.CANCELD),
    OrderState.PAID, Set.of(OrderState.DELIVERED)
  );
​
  public static void validate(OrderState latestState, OrderState nextState) {
    if (!nextStateMap.get(latestState).contains(nextState)) {
      throw new StateMachineValidationException();
    }
  }
}

这样,在执行UncompliantHandler.run()单元测试的时候,就会抛StateMachineValidationException异常。

状态机权限设计

上述代码示例把状态机校验规则写在Java代码里面,开发同学仍然有修改权限。所以可以把状态机状态转换规则写在配置文件里面,甚至加上权限控制。这样就能从源头上防止开发同学写出不合规的工作流了。

延展知识:事件溯源设计

在软件领域驱动设计中,有事件溯源的概念。简单说,就是不记录当下状态snapshot,而记录历史事件event_store。一个典型的使用场景是分布式记账。事件溯源牺牲了储存空间,但是能用版本控制,加了关锁的方式,解决并发冲突。 在订单状态案例中,按照事件溯源设计,就要额外增加order_event表格。order_event表格只允许新增,不允许修改。

CREATE TABLE order_event (
  order_id UUID,
  event_id SMALLINT, 
  state ASCII, -- enums like INITIATED, PAID, DELIVERED, CANCELED
  actor ASCII, -- enums like CUSTOMER, MERCHANT
  created TIMESTAMP,
  PRIMARY KEY (order_id, event_id)
);

其中,

  • order_id是外键,关联order表格。

  • 在查询的时候,往往根据主键order_id,一次性查询出所有的order_event。然后根据event_id,过滤出最新的订单状态。

  • 新增order_event的时候,如果有并发写问题,那么第二个线程在写入数据库的时候,会得到DuplicateKeyException,解决并发问题。

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值