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

时间:2015-12-16 05:25:29

标签: domain-driven-design cqrs event-sourcing

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

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

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

2 个答案:

答案 0 :(得分:4)

命令可能会失败。

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

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

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

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

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

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

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

答案 1 :(得分:2)

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

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

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

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

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