CQRS 和事件溯源的普及版

2作者: odinellefsen7 天前原帖
许多团队喜欢不可变事件日志的想法,但由于经典事件溯源需要聚合、每个实体的流和深度领域驱动设计,往往不予采纳。每次写入通常意味着需要重放数千个事件,以在内存中重建聚合,然后才能附加新事件。这确保了完美的一致性,但也提高了入门的门槛。 在领域驱动开发与事件溯源中,你需要设计一个聚合,例如订单(Order)。对于这个聚合,你设计领域事件,如订单创建(OrderCreated)、订单信息更新(OrderInfoUpdated)、订单归档(OrderArchived)和订单完成(OrderCompleted)。这意味着存储在订单聚合中的每个事件都是这些设计好的领域事件之一。在这一点上,你创建订单聚合的实例(系统中每个实际产品订单一个实例),例如 Order-001、Order-002,依此类推。对于每个实例,例如 Order-001,你附加与该订单在其事件流中发生的事件相对应的领域事件。 在将领域事件附加到事件流(即你的真实来源)之前,你必须确保用户操作是有效的。验证用户操作/命令是通过重新加载/重放相关聚合实例的每个过去事件来完成的。对于名为 BankAccount 的聚合及其聚合实例,例如 BankAccount-1234,可能会有数百万个领域事件/事件,这意味着每当有人对其银行账户进行操作时,验证该操作可能需要很长时间,这就是快照(snapshots)概念的用武之地,以加快这一过程。 重新加载整个事件历史的目的是为了重建你的应用程序当前状态,或者更具体地说,重建实体/聚合实例的当前状态,即 BankAccount 或 Order。这样做是为了确保你在验证新的用户操作时是基于最新的应用状态,而不是旧的应用状态。 还有另一种方法可以实现验证(并实现事件溯源的核心概念),它不需要你处理重新加载整个事件流的复杂性,也不需要设计聚合来验证新的用户操作。我将要解释的这种替代方案降低了 CQRS + 事件溯源的入门门槛,因为它消除了领域驱动设计的复杂性,并显著扩大了使用案例和可及性(一些经典用例可能不适合这种方法)。但同时,它需要一种不同且强大的基础设施。 我所建议的方法是重新利用领域事件,使其成为我们所称的事件类型(Event Types)的事件流。与其为每个单独的订单创建事件流,不如将每个创建、更新、归档或完成的订单归类到相应的事件类型中。这意味着在提供的示例中,你将为订单聚合拥有 4 个事件流,而不是为系统中的每个订单拥有一个事件流。 我实现事件溯源的方法是通过对实时读取模型进行简单的 SQL 业务逻辑检查。这些读取模型包含了我应用程序的最新状态,在高吞吐量的关键情况下,延迟为个位数毫秒,而在较小吞吐量的较少关键情况下,延迟为个位数秒。 这两种方法都使用应用程序的当前状态,要么通过调用读取模型,要么通过重新加载所有过去的事件来重建当前状态。重新加载只有在不同步的读取模型不可接受时才显得重要。生产数据库在 CQRS 中是下游服务,因此总会存在轻微的延迟。在高争用或超低延迟的领域,如真实货币转账,你应该重放单个账户流以避免风险。如果读取模型在几毫秒到几秒内更新,那么对其进行验证对于绝大多数应用程序来说是完全足够的。
查看原文
Many teams love the idea of an immutable event log yet never adopt it because classic Event Sourcing demand aggregates, per-entity streams, and deep Domain-Driven Design. Each write often means replaying thousands of events to rebuild an aggregate in memory before a new event can be appended. That guarantees perfect consistency, but it also raises the cost of entry.<p>In Domain Driven Development + Event Sourcing you design an Aggregate, for example Order. For the Aggregate you design Domain Events like OrderCreated, OrderInfoUpdated, OrderArchived, and OrderCompleted. This means that every Event stored for the Order aggregate is one of those designed Domain Events. At this point you create instances of the Order aggregate (one instance for each actual product order in the system). And this looks like Order-001, Order-002, and so on. For each instance, for example, Order-001, you append Domain Events corresponding to what has happened to that order in that orders event stream.<p>You have to make sure that a user action is valid before you append a Domain Event to the event stream (which is your source-of-truth). Validating a user-action&#x2F;Command is done by rehydrating&#x2F;replaying every past event for the aggregate instance in question. For an aggregate called BankAccount with it’s aggregate instances, i.e. BankAccount-1234, there can be millions of Domain Events&#x2F;events which can take a long time to rehydrate&#x2F;replay every time a person does an action on their bank account where you have to validate the action, which is where a concept called snapshots comes in to make this faster.<p>The point of rehydrating the entire event history is because you want to recreate the current state your application or more specifically the current state of the entity&#x2F;aggregate-instance, i.e. BankAccount or Order. You do this to be confident that you’re validating a new user action against the latest application state and not an old application state.<p>There is another approach to achieve validation (and achieve the core concept of event sourcing) that doesn’t require you to handle the complexity of rehydrating your entire event stream nor designing aggregates just to be able to validate a new user action. This alternative that I’m going to explain lowers the barrier to entry for CQRS + Event Sourcing because it removes DDD design complexity, and widens use-cases and accessibility significantly (some classic use-cases may not be a good fit for this approach). But at the same time it requires a different and strong infrastructure.<p>The approach I&#x27;m suggesting repurposes Domain Events to instead serve the function of being the stream of events what we call Event Types. Instead of having event streams for each individual order you’d group every created, updated, archived, or completed order in it’s respective Event Type. This means that for the provided example you’d have 4 event streams for the Order aggregate instead of having an event stream for every order in your system.<p>How I achieving Event Sourcing is by doing simple SQL business logic checks against real time Read Models. These contain the latest state of my application with a lag, in high-throughput critical situations, of single digit milliseconds, and in less critical smaller throughput situations, single digit seconds.<p>Both approaches use the current state of your application, either by calling the read model or by rehydrating all past events to recreate the current state. Rehydration really matters only when an out-of-sync Read Model is unacceptable. The production database is a downstream service in CQRS, so a slight delay always exists. In high-contention or ultra-low-latency domains such as real-money transfers you should replay a single account stream to avoid risk. If the Read Model is updated within a few milliseconds to a few seconds then validating against it is completely sufficient for the vast majority of applications.