为什么sagas(又名流程管理员)包含一个内部状态,为什么它们会持久存在于事件存储中?

问题描述 投票:5回答:2

很多关于CQRS的文章都暗示传奇有内部状态,必须保存到活动商店。我不明白为什么这是必要的。

例如,假设我有三个聚合:OrderInvoiceShipment。当客户下订单时,订单处理开始。但是,在发票已经支付并且货物已经准备好之前,不能发送货件。

  1. 客户使用PlaceOrder命令下订单。
  2. OrderCommandHandlerOrderRepository::placeOrder()
  3. OrderRepository::placeOrder()方法返回一个OrderPlaced事件,该事件存储在EventStore中并沿EventBus发送。
  4. OrderPlaced事件包含orderId并预先分配invoiceIdshipmentId
  5. OrderProcess(“saga”)收到OrderPlaced事件,创建发票并在必要时准备货物(在事件处理程序中实现幂等)。 6A。在某个时间点,OrderProcess收到InvoicePaid事件。它通过查询ShipmentRepository中的货物来检查货物是否已准备好,如果是,则发送货件。 6B。在某个时间点,OrderProcess收到ShipmentPrepared事件。通过查询InvoiceRepository中的发票来查看是否已支付发票,如果是,则发送货件。

对于那里所有经验丰富的DDD / CQRS / ES大师,你能告诉我我错过了什么概念以及为什么这个“无国籍传奇”的设计不起作用?

class OrderCommandHandler {
    public function handle(PlaceOrder $command) {
        $event = $this->orderRepository->placeOrder($command->orderId, $command->customerId, ...);
        $this->eventStore->store($event);
        $this->eventBus->emit($event);
    }
}

class OrderRepository {
    public function placeOrder($orderId, $customerId, ...) {
        $invoiceId = randomString();
        $shipmentId = randomString();
        return new OrderPlaced($orderId, $customerId, $invoiceId, $shipmentId);
    }
}

class InvoiceRepository {
    public function createInvoice($invoiceId, $customerId, ...) {
        // Etc.
        return new InvoiceCreated($invoiceId, $customerId, ...);
    }
}

class ShipmentRepository {
    public function prepareShipment($shipmentId, $customerId, ...) {
        // Etc.
        return new ShipmentPrepared($shipmentId, $customerId, ...);
    }
}

class OrderProcess {
    public function onOrderPlaced(OrderPlaced $event) {
        if (!$this->invoiceRepository->hasInvoice($event->invoiceId)) {
            $invoiceEvent = $this->invoiceRepository->createInvoice($event->invoiceId, $event->customerId, $event->invoiceId, ...);
            $this->eventStore->store($invoiceEvent);
            $this->eventBus->emit($invoiceEvent);
        }

        if (!$this->shipmentRepository->hasShipment($event->shipmentId)) {
            $shipmentEvent = $this->shipmentRepository->prepareShipment($event->shipmentId, $event->customerId, ...);
            $this->eventStore->store($shipmentEvent);
            $this->eventBus->emit($shipmentEvent);
        }
    }

    public function onInvoicePaid(InvoicePaid $event) {
        $order = $this->orderRepository->getOrders($event->orderId);
        $shipment = $this->shipmentRepository->getShipment($order->shipmentId);
        if ($shipment && $shipment->isPrepared()) {
            $this->sendShipment($shipment);
        }
    }

    public function onShipmentPrepared(ShipmentPrepared $event) {
        $order = $this->orderRepository->getOrders($event->orderId);
        $invoice = $this->invoiceRepository->getInvoice($order->invoiceId);
        if ($invoice && $invoice->isPaid()) {
            $this->sendShipment($this->shipmentRepository->getShipment($order->shipmentId));
        }
    }

    private function sendShipment(Shipment $shipment) {
        $shipmentEvent = $shipment->send();
        $this->eventStore->store($shipmentEvent);
        $this->eventBus->emit($shipmentEvent);
    }
}
domain-driven-design cqrs event-sourcing
2个回答
4
投票

命令可能会失败。

这是主要问题;我们首先聚合的全部原因是,它们可以保护业务免受无效状态变化的影响。那么如果createInvoice命令失败,onOrderPlaced()会发生什么?

此外(尽管有些相关)你会迷失方向。流程经理处理事件;事件是过去已经发生的事情。 Ergo - 流程经理过去一直在运行。在一个非常真实的意义上,他们甚至不能与任何已经看到比他们正在处理的事件更近的事件的人交谈(事实上,他们可能是第一个看到这个事件的处理者,这意味着其他人都是迈出过去)。

这就是为什么你不能同步运行命令;你的事件处理程序是过去的,除非它在当前运行,否则聚合不能保护其不变量。您需要异步分派才能使命令针对正确的聚合版本运行。

下一个问题:当您异步调度命令时,无法直接观察结果。它可能会失败,或者在途中迷路,事件处理程序也不会知道。它可以确定命令成功的唯一方法是观察生成的事件。

结果是,进程管理器无法区分失败的命令和成功的命令(但事件尚未可见)。为了支持有限的sla,你需要一个定时服务,不时唤醒流程管理器来检查事物。

当流程管理器唤醒时,需要状态才能知道它是否已完成工作。

有了国家,一切都变得如此简单易于管理。进程管理器可以重新发出可能丢失的命令,以确保它们通过,而不会使用已经成功的命令充斥域。您可以在不将时钟事件抛入域本身的情况下对时钟进行建模。


2
投票

您所指的似乎与编排(与流程管理器)和编排相关。

编舞工作非常好,但你不会有一个流程经理作为一流的公民。每个命令处理程序将确定要执行的操作。即使我当前的项目(2015年12月)也使用webMethods集成代理进行编排。消息甚至可以携带一些状态。但是,如果需要同时进行任何操作,那么您需要进行相应的操作。

相关的service orchestration vs choreography question非常好地展示了这些概念。其中一个答案包含一个很好的图形表示,并且如答案中所述,更复杂的交互通常需要该过程的状态。

我发现在与您无法控制的服务和端点交互时,您通常会要求状态。人工交互(例如授权)也需要这种类型的状态。

如果您没有专门为流程经理提供州,那么可能就行了。但是,稍后您可能会遇到问题。例如,某些低级/核心/基础结构服务可能跨越各种进程。这可能会导致编排场景中出现问题。

© www.soinside.com 2019 - 2024. All rights reserved.