这是一篇译文,译文首发于 事件驱动架构设计,转载请注明出处!
这篇文章是 软件架构演进 一个有关 软件架构 系列文章中的一篇。这些文章,主要是我学习软件架构、对软件架构的思考及使用方法的记录。相比于这个系列的前几篇文章,本篇文章可能看来更有意义。php
采用设计驱动开发应用程序的实践,能够追溯到 1980 年左右。咱们能够在前端或者后端采用事件驱动模型。好比点击一个按钮、数据变动或者某些后端服务被执行。html
可是究竟什么才是事件驱动呢?什么时候使用事件驱动?它有没有缺陷?前端
就像类和组件同样咱们应当在编码时实现高内聚低耦合。当须要组合使用组件时,好比 组件 A 须要触发 组件 B 中的某些逻辑,咱们天然而然的会想到在 组件 A 中去直接调用 组件 B 实例中的方法。然而,若是 A 须要明确知道 B 的存在,那么它们之间是耦合的,A 依赖于 B,这使得系统难以维护和迭代。事件驱动能够 解决耦合 的问题。程序员
此外,采用事件驱动的另一个好处是,若是咱们有一个独立的团队开发 组件 B,他们能够直接修改 组件 B 的业务逻辑而无需事先和研发 组件 A 的团队进行沟通。各个组件能够单独迭代:咱们的系统更变得有组织性。数据库
甚至,在同一个组建内,有时咱们的代码须要在一个 request 和 response 周期内,做为某个操做的结果被执行,可是又不须要当即被执行的相似处理。一个常见示例就是发送电子邮件。此时,咱们能够直接响应用户结果,而后以异步方式延迟发送一个电子邮件给用户,这样就避免了用户等待发送邮件的时间。编程
不过,即便这样处理依然存在风险。若是咱们胡乱使用事件驱动设计,咱们就有可能要承担中断业务逻辑的风险,由于这些业务逻辑具备概念上的高度内聚,却采用了解耦机制将它们联系在一块儿。换句话说,就是将本来须要组织在一块儿的代码强行分离,而且这样难于定位处理流程(好比使用 goto 语句),来理解业务处理:这就变成了 面条式的代码[1]。后端
为了防止咱们的代码变成一堆复杂的逻辑,咱们应当在某些明确场景下使用事件驱动架构。就个人经验来说,在如下 3 种场景下可使用事件驱动开发:设计模式
当组件 A 须要执行组件 B 中的业务逻辑,相比于直接调用,咱们能够向事件分发器中发送一个事件。组件 B 经过监听分发器中的特殊事件类型,而后当这类事件被触发时去执行它。缓存
这意味着组件 A 和组件 B 都依赖于事件分发器和事件,而无需关注彼此实现:即完成它们的解耦。安全
理论上,分发器和事件应该处在不一样的组件中:
共享内核
[...] 用明确的边界指定团队赞成共享的域模型的某些子集。保持这个内核很小。[...] 这个拥有特殊状态的明确的共享机制,不得在未经团队协商状况下随意修改。
Eric Evans 2014, Domain-Driven Design Reference
有时咱们会有一系列须要执行的业务逻辑,可是因为它们须要耗费至关长的执行时间,因此咱们不想看到用户耗费时间去等待这些逻辑处理完成。在这种状况下,最好将它们做为异步任务来运行,并当即向用户返回一条信息,通知其稍后继续处理相关操做。
好比,在网店下订单能够采用同步执行处理,可是发送通知邮件则采用异步任务去处理。
在这种状况下,咱们所要作的是触发一个事件,将事件加入到任务队列中,直到一个 worker 进程可以获取并执行这个任务。
此时,相关的业务逻辑是否处在同一个上下文中环境中并不重要,无论怎么说,业务逻辑都是被执行了。
在传统的数据存储的方式中,咱们经过实体模型(entities)保存数据。当这些实体模型中的数据发生变化时,咱们只需更新数据库中的行记录来表示新的值。
这里的问题是咱们没法准确存储数据的变动和修改时间。
咱们能够经过审计日志模型将包含修改的内容存入到事件里。
在关于事件来源的知识,咱们会作进一步的阐述。
在实现事件驱动的架构时,一个常见的争议是到底是使用 监听器(listener) 仍是 订阅者(Subscriber),这里谈谈个人见解:
Martin Fowler 定义了 3 种事件模式:
这三种模式核心是同样的:
假设,有一个应用在内核(core)中定义了一些组件。理想状况下,这些组件是彻底分离的,可是它们的一些功能须要在其余组件中去执行一些逻辑。
这是最典型的应用场景,前面已经讲过:当组件 A 执行时,须要触发组件 B 中的逻辑时,这里能够去触发一个事件将其发送到事件分发器中,而不是直接调用。组件 B 经过监听分发器中的这类事件,当有事件触发时去执行这个事件。
须要注意的是,这个模式的一个特征是 事件自己携带的数据非量常少。它只携带足够的数据,以便监听器知道发生了什么,并执行它们的代码,数据一般是实体模型的 ID,可能还有事件建立的日期和时间。
优势
缺点
仍是以前那个在内核中定义了一些组件的应用。此次,多于一些功能须要使用其它组件中的数据。获取数据的最天然方式是从其它组件中查询出数据,可是这也意味着这个组件知道被查询组件的存在:这样两个组件就偶合在一块儿了!
实现数据共享的另外一种方法是,当数据在所属组件中被变动时,触发一个事件。这个事件携带新版本中的全部数据。对该数据感兴趣的组件能够监听这类事件,并依据数据存储中的数据进行处理。这样当组件之间须要外部数据时,他们也可以获取本地副本,而无需从其它组件中查询。
优势
缺点
若是两个组件都在同一个进程中,可以快速的实现组件间通讯,那么实现这种设计模式可能就没那么必要了。不过为了实现组件分离或可维护性,或在将来的计划中将组件封装进不一样的微服务中使用这种模式。全部的一切取决于现有需求和计划,以及咱们但愿(或须要)将系统解耦到什么程度。
假设,如今有一个刚刚初始化的实体(Entity)。做为实体,它有本身的标识(identity),它对应现实世界中的某一事物,在程序中就是模型。在整个生命周期内,数据库仅仅简单的保存实体的当前状态。
多数场景下,这种存储方式是可行的,但若是咱们须要知道实体究竟如何到达当前这个状态(好比,咱们想知道银行帐户的贷方和借方)。这时候因为咱们仅存储当前状态,可能就没法实现这种需求了。
使用事件溯源模式替代实体状态存储,咱们关注实例状态的 变动 并 依据变动计算出实体状态。每一个状态的变化都是一个事件,被存储到事件流中(如 RDBMS 中的表)。当咱们须要获取实体的当前状态是,咱们经过计算这个事件的全部事件流来完成。
事件存储做为结果的主要来源,系统状态也单纯的转变成了它的派生结果。对程序员来讲,最好的例子是版本控制系统。全部的提交日志就是事件存储,当前源代码树的工做副本就是系统的状态。
Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
若是如今存在一个错误的状态变动(event),咱们不能简单的将其删除由于这样会改变状态的历史记录,这就与事件溯源的设计初衷背道而驰了。替代的方法是,咱们在事件流里建立一个新的事件,咱们将但愿删除的事件回退(reverses)到以前的状态。这个过程称之为事务回退,这个操做不只将实体恢复到指望的状态,还留下记录表名这个实体在给定的时间节点所处的状态。
不删除数据也有架构上的收益。存储系统成为一种仅添加的架构,众所周知,仅添加的架构比起可更新架构更容易部署,由于它要处理的锁要少得多。
Greg Young 2010, [CQRS Documents](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
不过,当在一个事件流中包含不少的事件时,计算实体状态则会变的代价高昂,还会严重影响性能。为了解决这个问题,每当产生 X 条事件时,咱们将在那个时间点建立实体状态的快照。甚至,咱们能够保存这个实体的永久更新过的快照,这样咱们就能同时拥有两个最优的平行世界。
在事件溯源中咱们还引入了 投影(projection) 的概念,它是必定时间范围内基于事件流计算后的事件结果。这就是快照,或者说实体的当前状态,这就是投影的定义。可是在 投影 这个概念中最有价值的是,咱们能够经过分析特定时间内的实体「行为」,实现对将来的行为做出预测(好比,在过去 5 年里实体模型都在 8 月份增长了活动量,那么它颇有可能在明年 8 月份产生一样的行为)。这对企业来讲是一个颇有价值的能力。
事件溯源在商业和软件开发过程这两方面很是有用:
然而,并不是一切都如此美好,警戒以下问题:
当事件在外部系统中触发更新时,咱们不但愿在回放事件以建立投影时从新触发这些事件。此时,咱们只需在 「回放模式」中禁用外部更新,能够将这个逻辑封装到网关里实现。
另外一种解决方案依赖于实际的问题,能够将更新缓存(buffer)到外部系统,在一段时间后执行更新,这时能够安全地假设事件不会回放。
当在外部系统中使用查询来检索咱们的事件时,好比获取股票债券评级,当咱们回放事件来建立投影时会发生什么呢? 咱们可能想要获得与事件第一次运行时相同的评分,这也许是几年前生成的。所以,远程应用能够给咱们这些值,或者咱们须要将它们存储在咱们的系统中,这样咱们就能够经过封装网关中的逻辑来模拟远程查询。
Martin Fowler 定义了 3 种类型的代码变动:新特性(new features),bug 修复和临时逻辑。真正的问题出如今回放事件时,这些事件应该在不一样的时间点使用不一样的业务逻辑规则,好比,去年的税收计算就与今年的不一样。一般状况下,可使用条件语句,可是这回使逻辑变得混乱,因此建议使用策略模式。
个人建议是谨慎使用这个模式,通常我会尽可能遵循以下原则:
固然,和其它模式同样,并不是任什么时候候均可以使用它,当使用比不适用带来更多收益时,咱们应该去使用这种模式。
事件驱动架构核心在于封装、高内聚和低耦合。
事件驱动能够提高代码的可维护性、性能和业务增加的需求,可是,经过事件溯源模式,还能提升系统数据的可靠性。
不过,事件驱动一样存在弊端,由于不管是概念上的复杂度仍是技术上的复杂度都增长了,当它被滥用时将致使灾难性的后果。
2005 • Martin Fowler • Event Sourcing
2006 • Martin Fowler • Focusing on Events
2010 • Greg Young • CQRS Documents
2014 • Greg Young • CQRS and Event Sourcing – Code on the Beach 2014
2014 • Eric Evans • Domain-Driven Design Reference
2017 • Martin Fowler • What do you mean by “Event-Driven”? 中译 中译2
2017 • Martin Fowler • The Many Meanings of Event-Driven Architecture
[1] 面条式代码(Spaghetti code)是软件工程中反面模式的一种 (1),是指一个源代码的控制流程复杂、混乱而难以理解 (2),尤为是用了不少 GOTO、例外、线程、或其余无组织的分支。其命名的缘由是由于程序的流向就像一盘面同样的扭曲纠结。面条式代码的产生有许多缘由,例如没有经验的程序设计师,及已通过长期频繁修改的复杂程序。结构化编程可避免面条式代码的出现。这样,当咱们须要获取实体状态时,只须要计算最后一个快照便可。