事件溯源为什么事件溯源是微服务通信反模式
领域事件是通用语言的核心
用于跨边界通信的领域事件
脱钩神话
简单的答案:事件源!
事件溯源只是一种持久策略吗?
有出路
选择的自由
结论
参考
事件驱动架构,尤其是事件溯源,在过去几年中获得了广泛的关注。这种趋势源于我们致力于构建具有弹性和可扩展性的模块化系统。微服务是这种背景下经常使用的术语。在我看来,微服务只是实现有界上下文的一种方式。模块化系统的核心是模块的边界,而识别这些边界最有希望的想法是Eric Evans 在领域驱动设计中提出的战略设计。它可以帮助您识别/发现具有边界(有界上下文)的模块,并描述这些有界上下文之间的关系(上下文映射)。
注意:由于我不想重复解释某些术语,因此我会假设读者已经了解它们。我决定提供microservices.io、维基百科或Martin Fowler 的 Bliki的解释链接,因此,如果您对现有知识感到不适,您可以自行深入研究某个主题。
领域事件是通用语言的核心
虽然 Eric 的著作《领域事件》中没有明确提及,但它确实很好地阐释了 DDD 的概念。像 Alberto Brandolini 的《事件风暴》这样的技术将事件的焦点从技术层面转移到了组织/业务层面。我们讨论的不是像ButtonClickedEvent这样的 UI 层事件,而是领域事件。这些领域事件是业务领域的一部分,业务专家能够理解并运用它们。这些领域事件是一流的概念,它们提供了一种构建所有参与者(领域专家、开发人员等)都认可的通用语言的绝佳方式。
用于跨边界通信的领域事件
领域事件可用于促进有界上下文之间的通信。假设我们有一个在线商店,其中包含三个有界上下文:订单、配送、发票。
订单上下文的一个领域事件是订单被接受。发票上下文和交付上下文都对此事件的发生感兴趣,因为它会引发一些内部流程的启动。
脱钩神话
使用领域事件可以帮助您开发解耦的模块。模块可能会暂时离线。领域事件不关心不可用的模块,它们描述的是过去发生的事情。其他模块处理事件的速度取决于它们。您将获得一个设计精良、弹性的系统。
除了时间解耦之外,领域事件至少乍一看还有另一个优势:
订单上下文无需知道发票上下文和交付上下文监听了它的事件。实际上,它甚至不需要知道这些上下文的存在。
这很酷,但挑战在于事件负载。要将哪些数据放入事件中?
简单的答案:事件源!
事件很有用,那么为什么不赋予它们尽可能多的权力(和责任)呢?这就是事件溯源的基本思想。你不是通过更新数据(CRUD)来存储聚合的状态,而是通过应用事件流来存储。
除了可以重放事件来重建应用程序状态之外,事件溯源的一大优势是可以免费获得完整可靠的审计日志。因此,当需要这样的审计日志时,在评估持久化策略时,绝对应该考虑事件溯源。
事件溯源只是一种持久策略吗?
您可能想知道为什么我从领域事件直接转到持久性策略,因为这些概念显然在不同的层/抽象级别上起作用。
……这就是我的观点:事件溯源是由单个限界上下文做出的本地决策!事件不应该暴露给外界!其他限界上下文彼此之间不知道彼此的持久化策略,因此它们不知道也不会关心其他限界上下文是否使用了事件溯源。
如果您在全球范围内使用事件源,则会暴露您的持久层。
你的持久化数据变成了你的公共 API。每当有界上下文调整其持久化数据时,我们都必须处理公共 API 的变更。
我确信大家都同意,不同的限界上下文共享(关系型)数据库中的数据是个坏主意,因为开发和运行时会相互耦合。但区别在哪里呢?
没有。无论我们共享事件还是数据库表都没关系。在这两种情况下,我们都共享持久化细节。
有出路
我仍然认为领域事件非常适合有界上下文之间的通信,但这些事件不应与用于事件源的事件相对应。
我提出的解决方案符合逻辑:无论您使用 CRUD 还是事件溯源方法进行持久化,您都需要将领域事件发布到全局事件存储中。这些领域事件是您的限界上下文的公共 API。如果您更喜欢在限界上下文中使用事件溯源,则需要将这些事件存储在本地事件存储中,并且只能从该限界上下文访问。
选择的自由
在你的公共 API 中拥有专用的领域事件,可以让你自由地决定如何建模这些事件。你不再受限于事件源事件预定义的布局。
对于每次发生的“现实世界事件”,您有两个选择:
使用已发布语言的开放主机服务
发布一个领域事件,该事件包含其他限界上下文可能需要的所有数据。在领域驱动设计 (DDD) 术语中,我们称之为“具有已发布语言的开放主机服务”。
现实世界事件Order accepted的发生会引发一个已发布的领域事件OrderAccepted。该事件的有效负载包含了 Order 期望其他有界上下文感兴趣的所有数据……因此,希望 Invoice 和 Delivery 上下文能够找到它们所需的所有信息。
客户/供应商
发布多个专用领域事件,每个事件消费者一个。您只需与另一方(消费者)讨论每个特定的领域事件,而无需定义共享模型。DDD 将这种关系称为“客户/供应商”。
现实世界事件Order accepted的发生会导致每个消费限界上下文发布一个领域事件:InvoiceOrderAccepted和DeliveryOrderAccepted。每个领域事件都包含消费上下文所请求的数据。
我不想讨论这两个选项的优缺点。我只想强调,您可以自由选择域事件的数量及其有效负载。
这是一个不容低估的巨大优势,因为您可以决定如何发展有界上下文的 API,而不必致力于事件源所需的事件。
结论
将持久化细节暴露给外界是一种众所周知的反模式。谈到持久化,我们首先想到的是数据库表,但我解释了为什么事件溯源中使用的事件只是另一种持久化数据的方式。因此,暴露这些事件也是一种反模式。
如果以恰当的(本地)方式使用,事件溯源功能非常强大。乍一看,它似乎是事件驱动架构的灵丹妙药,但如果你深入研究,就会意识到它可能会导致系统紧耦合(分布式)……而这肯定不是你的目标。
参考
除了个人经验之外,我从不同的摘要和会议演讲中汲取了很多灵感。我想特别推荐 Eberhard Wolff 的演讲《基于事件的架构与 Kafka 和 Atom 的实现》 。尤其是“事件溯源”和“事件中有什么?”这两章与本文的上下文高度相关。我选择的在线商店示例也受到了这次演讲的启发。
如果您想获取更多信息,可以参考其他一些资源:
- 领域事件与事件溯源,作者:Christian Stettler,博客文章
- Hugo Rocha 的博客文章《他们没有告诉你的事件溯源》
- 十年 DDD、CQRS、事件溯源(CQRS/ES 不是顶级架构)作者:Greg Young,会议演讲