文 / Martin Fowler
译 / 梅雪松
去年年底(译者注: 2016 年底), 我和 ThoughtWorks 同事一起参加了一个研讨会, 讨论 "事件驱动" 的本质. 在过去的几年里, 我们构建的很多系统都大量使用了事件. 对于这些系统, 人们常常赞誉有加, 但批评的声音也不绝于耳. 我们的北美办公室组织了一次峰会, 来自世界各地的 ThoughtWorks 资深开发者出席会议并分享了他们的想法.
这次峰会的最大认识是到当人们谈论 "事件" 时, 实际上说的是完全不同的东西, 所以我们花了很多时间来梳理一些有用的模式. 本文简要总结我们的成果.
事件通知
当领域内有变化发生时, 发送事件消息来通知其它系统. 事件通知的一个关键点是源系统并不关心外部系统的响应. 通常它根本不期待任何结果, 即使有也是间接的. 发送事件的逻辑流与响应该事件的逻辑流之间会有显著的隔离.
事件通知非常有用, 因为它意味着低耦合, 并且结构也非常简单. 但是, 当逻辑处理流跨越各种事件通知时, 它也可能成为问题. 因为没有任何代码显式地描述这个流程, 所以这个流程是不可见的. 通常, 唯一的办法是通过监控系统来观察它. 这会导致调试和修改流程变得很困难. 这里的危险在于, 当你使用事件通知来优雅地做系统解耦时, 没有意识到更大规模的流程, 而这会让你在未来几年中陷入困境. 不论如何, 此模式仍然非常有用, 但你必须小心陷阱.
举个例子, 将事件用作被动操控型命令 (Passive-aggressive command) 就属于这种陷阱. 它指的是源系统期待接收方执行一个动作, 此时本该使用命令消息 (Command message) 来展现此意图, 然而却使用了事件.
事件不需要包含太多数据, 通常只有一些 ID 信息和一个指向发送方, 可供查询更多信息的链接. 接收方知道它已发生变化, 并且接收到关于变化的最少信息, 随后会向发送方发出请求, 以决定下一步该做什么.
事件携带的状态转移(Event-Carried State Transfer)
采用此模式时, 可以在不需要访问源系统的情况下, 更新客户端的信息. 客户管理系统可能在客户修改自己的详细信息 (如地址) 时抛出事件, 事件包含了详细的修改数据. 因此, 接收方无需与客户管理系统通信, 就可以更新自己的客户数据副本, 以进行下一步的操作.
这种模式的一个明显缺点是, 有很多冗余数据和副本. 但在存储很便宜的时代, 这不是一个问题. 我们获得了更好的弹性, 因为即使客户管理系统不可用时, 接收方系统仍然可以正常工作. 我们减少了延迟, 因为访问客户信息不需要远程调用. 我们也不必担心所有来自消费端的查询给客户管理系统带来的负载. 但它确实给事件接收端带来了更多复杂性, 因为它必须维护所有状态, 而如果它直接访问事件发送方查询信息, 通常会更加容易.
事件溯源
事件溯源 (Event Sourcing https://martinfowler.com/eaaDev/EventSourcing.html ) 的核心思想是, 每当系统状态发生变化时, 都将状态更改记录为事件, 这样我们就有信心在任何时间都能够通过重新处理事件来重建系统状态. 事件库成为事实的主要来源, 系统状态完全来源于它. 对于程序员来说, 最好的例子就是版本控制系统. 所有的提交日志就是事件库, 源码树的工作副本是系统状态.
事件溯源会引入很多问题, 但我不会在这里讨论, 我想强调一些常见的误解. 事件处理不必是异步的, 以更新本地 Git 库为例, 这完全是一个同步操作, 就像更新 Subversion 这样的集中式版本控制系统一样. 当然拥有所有这些提交允许你做各种有趣的事情, Git 就是一个很好的例子, 但核心提交从根本上说是一个简单的动作.
另一个常见错误是, 假定使用事件溯源系统的每个人都应该理解并访问事件日志以确定有用的数据, 但实际上他们很可能对事件日志只具备有限的了解. 我正在使用编辑器写这篇文章, 编辑器不知道我的源代码树中的所有提交, 它只是假设磁盘上有一个文件. 在基于事件溯源的系统中, 很多处理可以基于一个有效的工作副本. 只有当真正需要事件日志中的信息时才必须处理它. 如果需要的话, 我们可以有多个不同 Schema 的工作副本, 但通常应该在领域处理和通过事件日志派生工作副本之间做明确区分.
使用事件日志时, 构建工作副本的快照通常很有用, 这样你就不必在每次需要工作副本时都从头开始处理所有事件. 实际上这里存在二元性, 我们可以将事件日志视为变更列表或状态列表. 我们可以从一个派生出另一个. 版本控制系统通常在事件日志中混合快照和增量变更, 以获得最佳性能.[]
考虑一下版本控制系统带来的价值, 就很容易明白事件溯源有许多有趣的收益. 事件日志提供了强大的审计功能 (账户交易是帐户余额的事件溯源). 我们可以重放事件日志到某个点来重新创建历史状态. 在重放时注入假设事件可以探索不一样的历史. 事件溯源使得非持久化的工作副本(例如 Memory Image https://martinfowler.com/bliki/MemoryImage.html ) 变得合理可行.
事件溯源也有自己的问题. 当结果依赖于与外部系统的交互时, 重放事件就会成为问题. 随着时间的推移, 我们必须清楚如何处理事件 Schema 的变化. 许多人发现事件处理给系统增加了很多复杂性(尽管我很想知道, 主要原因是不是工作副本派生组件和领域处理组件之间糟糕的隔离).
CQRS
命令查询职责分离 ( https://martinfowler.com/bliki/CQRS.html ) 是指读取和写入分别拥有单独的数据结构. 严格地说, CQRS 跟事件没有关系, 因为你完全不需要任何事件就可以使用 CQRS. 但通常人们会将 CQRS 与之前的模式结合起来, 因此我们在峰会上就此进行了讨论.
使用 CQRS 的理由是, 在复杂领域中, 使用单一模型处理读取和写入过于复杂, 我们可以通过分离模型来简化. 当访问模式有区别时(例如大量读取和非常少的写入), 这一点尤其具有吸引力. 但是, 需要注意平衡 CQRS 的收益和分离模型所带来的额外复杂度. 我发现很多同事对使用 CQRS 非常警惕, 发现它经常被滥用.
理解这些模式
作为一名热衷于收集样本的 "软件植物学家", 我发现这是一个棘手的地带, 主要问题在于不同模式的混淆. 在某个项目中, 一位能力很强, 经验丰富的项目经理告诉我, 事件溯源是一场灾难, 任何变化都需要两倍的时间来修改读和写模型. 在他这句话中, 可以发现事件溯源和 CQRS 之间可能存在混淆, 我们如何找出哪个是罪魁祸首? 该项目的技术主管声称主要问题是大量的异步通信, 这当然是一个已知的复杂性助推器, 但异步通信不是事件溯源或 CQRS 的必要组成部分. 总的来说, 我们必须要注意这些模式在对的地方都很好, 反之则很糟糕. 但是当我们混淆了这些模式时, 很难弄清楚哪里是对的地方.
来源: https://juejin.im/post/5c887b90f265da2d96184d66