业务开发中的状态机与 ago 之道

在实际业务开发中经常会采用状态机,例如一个简单的订单处理:

enum OrderState { CREATED, PAID, SHIPPED, COMPLETED, CANCELLED }
enum MsgType    { PAY_SUCCESS, SHIP_CONFIRM, DELIVER_CONFIRM, CANCEL_REQ }

class OrderStateMachine {
    private OrderState state = OrderState.CREATED;

    void process(Message msg) {
        switch (state) {
            case CREATED:
                switch (msg.type) {
                    case PAY_SUCCESS:   state = OrderState.PAID; break;
                    case CANCEL_REQ:    state = OrderState.CANCELLED; break;
                }
                break;
            case PAID:
                switch (msg.type) {
                    case SHIP_CONFIRM:  state = OrderState.SHIPPED; break;
                }
                break;
            case SHIPPED:
                switch (msg.type) {
                    case DELIVER_CONFIRM: state = OrderState.COMPLETED; break;
                }
                break;
            // 其他状态分支...
        }
    }
}

认识的人都知道,我很喜欢状态机。记得多年以前我就用状态机实现计算器,就是菜市场用的那种计算器,状态机应对这种同一个按钮有时这个意思、有时那个意思效果的场景非常好,当时还得到领导特别欣赏。

但是业务场景里的状态机和GUI设计、语法解析、信号处理等场景的状态机都不同。在 GUI 设计里,状态经常会回到起点,或者干脆就在原地,例如在计算器刚打开时不停的按 0,状态就始终还是初始状态。

而在业务场景里,状态往往是单向的,例如订单,从下单到支付发货,如果发生退货,无非也就是分两条叉。

需要说明的是,在 SAGA 流程里状态不会回来。SAGA 流程,例如退货,虽然动作是补偿动作,但状态是另一个状态,并非正向走时的那些状态,而是有一套相反的状态。

为什么业务开发需要状态机呢?其实状态机在业务开发领域并不能发挥它的真实实力。这是因为在业务场景里每进入一个状态都意味着业务告一段落,需要新的消息进行下一步处理,程序员将当前状态持久化,在下一个消息到来时就知道从哪个环节继续推进,相当于游戏的存盘点。

有的人可能会用同一个接口处理所有状态:

@PostMapping("/order/process")
public void handleAll(@RequestBody Message msg) {
    Order order = orderRepo.findById(msg.orderId);
    
    if (order.getState() == CREATED && msg.type == PAY_SUCCESS) {
        // 扣库存、生成支付流水、更新状态...
        order.setState(PAID);
    } else if (order.getState() == PAID && msg.type == SHIP_CONFIRM) {
        // 调用物流接口、打印面单、更新状态...
        order.setState(SHIPPED);
    } else if (order.getState() == SHIPPED && msg.type == DELIVER_CONFIRM) {
        // 结算佣金、触发评价提醒、完成订单...
        order.setState(COMPLETED);
    } else if (msg.type == CANCEL_REQ) {
        // 退款审批、恢复库存、发送通知...
        order.setState(CANCELLED);
    }
}

有的人可能会根据状态不同用不同的接口:

@PostMapping("/order/pay")      public void handlePay(@RequestBody PayMsg msg) { /* ... */ }
@PostMapping("/order/ship")     public void handleShip(@RequestBody ShipMsg msg) { /* ... */ }
@PostMapping("/order/deliver")  public void handleDeliver(@RequestBody DeliverMsg msg) { /* ... */ }

当然,现在还有很多技术形态。但不管怎么说,一个个从上到下的业务流程就这样分散在这些零散的状态之间。你再也无法看到一个从上到下的完整流程。

为此,有人采用规则引擎或 SEATA 设计器之类的东西将它们可视化,形成 DAG(有向无环图),这样就可以通过图形直观的看到一个订单活动是如何推进。

实际上,很多规则引擎就是依赖于状态机的,它会自动根据 DAG 生成状态机,记忆运行到了什么节点,下次继续运行。

通过规则引擎串联起微服务,这是一种可行的选择。然而规则引擎毕竟不是程序语言,它的代码是一堆对节点和边的描述,或为 JSON 或为 XML,并不是程序语言。例如

{
  "workflow": "order_lifecycle",
  "version": "1.0",
  "nodes": [
    { "id": "start_created",   "type": "START" },
    { "id": "action_pay",      "type": "SERVICE", "ref": "payment-service.pay" },
    { "id": "action_ship",     "type": "SERVICE", "ref": "logistics-service.ship" },
    { "id": "end_completed",   "type": "END" }
  ],
  "edges": [
    { "from": "start_created", "to": "action_pay",      "condition": "${msg.type == 'PAY_SUCCESS'}" },
    { "from": "action_pay",    "to": "action_ship",     "condition": "${msg.type == 'SHIP_CONFIRM'}" },
    { "from": "action_ship",   "to": "end_completed",   "condition": "${msg.type == 'DELIVER_CONFIRM'}" }
  ],
  "compensation": [
    { "nodeId": "action_pay",  "rollbackRef": "payment-service.refund" },
    { "nodeId": "action_ship", "rollbackRef": "logistics-service.cancel" }
  ]
}

可以发现,这种代码对软件工程来说可以说连 BASIC 语言都不如,所有软件复用技术,什么函数、类型、版本追踪等等全都失效了。

换 ago 该怎么写呢?

fun processOrder(orderInfo as OrderInfo) with Task{
    var order = new Order(orderInfo);
    order.setState(CREATED);    // 用于向外展示状态,流程不依赖它
    var msg = getMessage(order);
    if(msg.type == PAY_SUCCESS){
        // 扣库存、生成支付流水、更新状态...
        order.setState(PAID);
    } else {
        ...
        return
    }

    msg = getMessage(order);
    if(msg.type == SHIP_CONFIRM)
    {
        
    } else {
        ...
    }
}

fun getMessage(order as Order) as Message native "...";

让 WorkflowEngine 运行它即可。

为什么 ago 可以用一个函数来串起业务,不怕崩溃吗?

这是因为 ago 函数调用产生的 CallFrame 被识别到派生了 Task 后,WorkflowEngine 将在运行该 CallFrame 前持久化、在调用 getMessage 前持久化,在调用 getMessage 后回到 processOrder 持久化,只要 getMessage 做好幂等,业务就能顺利运行完成。

这个持久化包括哪些数据?它包括:调用帧的所有变量、程序计数器 pc 值,另外它也保存 RunSpace 当前的调用帧,如果函数有 parent scope,还会 scope 对象。

这里最危险的是 getMessage,如果 getMessage 一直卡在内存里,即使不阻塞线程,也会导致大量内存浪费。这是 coroutine 经常遇到的陷阱,当内存里有海量 coroutine 后,它和它的堆栈都堆在内存里,造成极大浪费。幸运的是在 ago 里 native 函数可以做到彻底释放内存。

怎么做呢?getMessage 函数可以这样实现,进入 getMessage 后让引擎执行订阅消息,随后让 RunSpace 进入等待状态并退出,这样内存就能完全释放。当引擎收到消息后,根据订阅恢复 RunSpace,getMessage 就能得到新消息作为返回了。

这里 message 可以指 MQ 的 message 也可以是 HttpRequest 等别的 input,取决于业务需要。

可以看到,ago 作为解释器框架对 Java 程序员来说具有强大的定制化能力,像这种持久化几乎不需要业务程序员做什么工作。

ago 作为一门功能完整的语言,支持编程语言的抽象能力。你可以把 processOrder 拆分成几个子函数,也可以拆出能复用的子函数,甚至将内部一些数据封装成类。

最后,ago 也支持将函数挪到另一个节点运行,只需要将 Task 换成 RunAt::(node) 即可指定适合运行它的节点,例如对于需要调用金融微服务的函数,可以让它挪到金融相关的节点跑。

对于业务程序员来说,只要编写这样一个函数,就能将整个业务尽入掌握,这就是 ago 语言“函数即类,调用帧即对象”的魅力。


ago 论文:https://doi.org/10.5281/zenodo.20919493
ago 网站:https://siphonlab.github.io/
ago 源码:https://github.com/siphonlab/ago

posted @ 2026-07-09 20:14  Inshua  阅读(25)  评论(0)    收藏  举报