“事件驱动”是什么意思?
2017年2月7日
去年年底,我参加了 Thoughtworks 同事组织的一个研讨会,讨论“事件驱动”应用程序的本质。在过去的几年里,我们一直在构建大量使用事件的系统,这些系统既受到赞扬,也受到批评。我们的北美办事处组织了一次峰会,来自世界各地的 Thoughtworks 高级开发人员齐聚一堂,分享想法。
峰会最大的成果是认识到,当人们谈论“事件”时,他们实际上指的是一些截然不同的事情。因此,我们花了很多时间试图梳理出一些有用的模式。这篇笔记简要总结了我们确定的主要模式。
事件通知
当系统发送事件消息以通知其他系统其域中的更改时,就会发生这种情况。事件通知的一个关键要素是源系统并不真正关心响应。通常它根本不期望任何答案,或者如果源系统确实关心响应,则响应是间接的。发送事件的逻辑流与响应对该事件的某些反应的任何逻辑流之间将存在明显的区别。
事件通知很好,因为它意味着低耦合,并且设置起来非常简单。但是,如果确实存在跨多个事件通知运行的逻辑流,则可能会出现问题。问题在于,很难看到这样的流程,因为它在任何程序文本中都不是显式的。通常,找出此流程的唯一方法是监视实时系统。这可能会使调试和修改此类流程变得困难。危险在于,使用事件通知很容易构建良好解耦的系统,而没有意识到您正在忽略更大规模的流程,从而在未来几年给自己带来麻烦。该模式仍然非常有用,但您必须注意陷阱。
这个陷阱的一个简单例子是当一个事件被用作被动攻击命令时。当源系统期望接收方执行操作,并且应该使用命令消息来表明该意图,但将消息样式设置为事件时,就会发生这种情况。
事件不需要携带太多数据,通常只是一些标识信息和指向发送方的链接,可以查询该链接以获取更多信息。接收方知道发生了某些变化,可能会获得有关变化性质的一些最少信息,但随后会向发送方发出请求以决定下一步做什么。
事件携带状态转移
当您想要更新系统的客户端,以便他们不需要联系源系统即可进行进一步的工作时,就会出现这种模式。客户管理系统可能会在客户更改其详细信息(例如地址)时触发事件,其中包含已更改数据的详细信息。然后,接收方可以使用更改更新其自己的客户数据副本,以便它将来永远不需要与主客户系统通信以完成其工作。
这种模式的一个明显的缺点是有大量数据被拖来拖去,并且有很多副本。但在存储空间充足的时代,这已经不是什么大问题了。我们获得的是更高的弹性,因为如果客户系统不可用,接收系统可以正常运行。我们减少了延迟,因为访问客户信息不需要远程调用。我们不必担心客户系统上的负载来满足所有消费者系统的查询。但这确实涉及接收方更复杂,因为它必须解决维护所有状态的问题,而通常情况下,在需要时调用发送方获取更多信息会更容易。
事件溯源
事件溯源的核心思想是,每当我们更改系统状态时,我们都会将该状态更改记录为一个事件,并且我们可以通过在将来的任何时间重新处理事件来自信地重建系统状态。事件存储成为主要的事实来源,系统状态完全源自它。对于程序员来说,最好的例子是版本控制系统。所有提交的日志是事件存储,源代码树的工作副本是系统状态。
事件溯源引入了很多问题,我在这里不做赘述,但我确实想强调一些常见的误解。事件处理不需要是异步的,请考虑更新本地 git 存储库的情况 - 这完全是一个同步操作,更新像 subversion 这样的集中式版本控制系统也是如此。当然,拥有所有这些提交可以让您进行各种有趣的操作,git 就是一个很好的例子,但核心提交本质上是一个简单的操作。
另一个常见错误是假设每个使用事件溯源系统的人都应该理解和访问事件日志来确定有用的数据。但是对事件日志的了解可能是有限的。我正在一个编辑器中编写这篇文章,它不知道我的源代码树中的所有提交,它只是假设磁盘上有一个文件。事件溯源系统中的大部分处理都可以基于有用的工作副本。只有真正需要事件日志中的信息的元素才需要对其进行操作。如果这有帮助,我们可以拥有多个具有不同模式的工作副本;但通常应该在域处理和从事件日志派生工作副本之间进行明确区分。
使用事件日志时,构建工作副本的快照通常很有用,这样您就不必每次需要工作副本时都从头开始处理所有事件。实际上,这里有一个二元性,我们可以将事件日志视为更改列表,也可以视为状态列表。我们可以从另一个派生出一个。版本控制系统通常在其事件日志中混合使用快照和增量,以便获得最佳性能。[1]
事件溯源有很多有趣的好处,在想到版本控制系统的价值时很容易想到。事件日志提供了强大的审计功能(会计交易是帐户余额的事件源)。我们可以通过重放事件日志到某个点来重新创建历史状态。我们可以在重放时通过注入假设事件来探索替代历史。事件溯源使得拥有非持久性工作副本变得合理,例如内存映像。
事件溯源确实存在问题。当结果取决于与外部系统的交互时,重放事件就会出现问题。我们必须弄清楚如何处理事件模式随时间的变化。许多人发现事件处理给应用程序增加了很大的复杂性(尽管我想知道这是否更多是由于派生工作副本的组件和执行域处理的组件之间分离不佳造成的)。
CQRS
命令查询责任隔离 (CQRS) 是指为读取和写入信息使用单独的数据结构的概念。严格来说,CQRS 与事件无关,因为您可以在设计中没有任何事件的情况下使用 CQRS。但通常人们会将 CQRS 与这里之前的模式结合起来,因此它们出现在峰会上。
CQRS 的理由是,在复杂的域中,用于处理读取和写入的单个模型变得过于复杂,我们可以通过分离模型来简化。当您的访问模式不同时,例如大量读取和极少写入,这尤其吸引人。但是,使用 CQRS 的收益必须与拥有单独模型的额外复杂性相平衡。我发现我的许多同事都对使用 CQRS 持谨慎态度,认为它经常被误用。
理解这些模式
作为一名热衷于收集样本的软件植物学家,我发现这是一个棘手的领域。核心问题是混淆了不同的模式。在一个项目中,能力强、经验丰富的项目经理告诉我,事件溯源是一场灾难——任何更改都需要两倍的工作来更新读取和写入模型。就在这句话中,我可以察觉到事件溯源和 CQRS 之间可能存在混淆——那么我该如何找出罪魁祸首呢?该项目的技术负责人声称,主要问题是大量的异步通信,这当然是一个已知的复杂性助推器,但它不是事件溯源或 CQRS 的必要部分。此外,我们必须注意,所有这些模式在正确的地方都是好的,而在错误的地形上则是不好的。但是,当我们合并这些模式时,很难确定什么是正确的地形。
我很想写一些权威性的论文来解决所有这些混乱,并给出关于如何做好每种模式以及何时应该使用它的可靠指南。可悲的是,我没有时间去做。我写这篇笔记是希望它会有用,但我很清楚它远远没有达到真正需要的水平。
延伸阅读
我准备了一个关于这个主题的演讲,它是 2017 年芝加哥 goto 大会上的主题演讲。
早在 2006 年,我就写了一些原型模式,并考虑为我的P of EAA 书编写更多内容。可悲的是,即使十年过去了,我仍然没有时间继续这项工作。然而,我当时写的东西在那里可以阅读。对于事件,我将从关注事件开始,它总结了我当时对使用事件的思考。虽然这是一段时间以前的事了,但我认为我当时写的大部分内容仍然有效。
这些文章中最有影响力的是关于事件溯源的文章。它主要讨论使用重放来形成历史状态和替代状态的价值。
关于事件协作的文章触及了事件通知和事件携带状态转移的模式,但混淆了这些模式(直到研讨会期间我才开始将它们视为独立的)。
我在CQRS上有一篇博客文章。
网上有很多关于这些主题的资料,所以尽情探索吧。我在这里没有任何评论,因为我没有花时间浏览它并挑选出一些建议。
脚注
1: 我有时会听到人们说 git 不是事件溯源的例子,因为它将文件和树的状态存储在.git/objects
中。但是系统是使用更改还是快照进行内部存储并不影响它是否是事件溯源的。Git 很乐意根据我的要求提供更改列表。当它将数据压缩到包文件中时,它确实使用了快照和更改的组合,并根据性能原因选择了混合。
更新
2017-02-08:调整了对事件日志与应用程序状态的使用讨论,并添加了脚注以阐明 git 和快照的作用。