工作流编排
在电子商务领域,一个订单的生命周期,往往长达数天,涉及到创建,支付,履约等行为和状态。怎么管理订单状态呢?如果任何一个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,解决并发问题。