龙空技术网

软件架构(17)-事件驱动架构

架构师狂飙 1128

前言:

现时你们对“php事件驱动”可能比较着重,各位老铁们都想要剖析一些“php事件驱动”的相关资讯。那么小编在网上搜集了一些对于“php事件驱动””的相关文章,希望大家能喜欢,我们快快来学习一下吧!

使用事件来设计应用程序似乎是 20 世纪 80 年代后期出现的一种做法。我们可以在前端或后端的任何地方使用事件。当按下按钮时,当某些数据更改或执行某些后端操作时。

但它到底是什么?我们什么时候应该使用它以及如何使用它?缺点是什么?

什么/什么时候/为什么

就像类一样,组件之间应该具有低耦合性,但在它们内部具有高内聚性。当组件需要协作时,假设组件“A”需要触发组件“B”中的某些逻辑,自然的做法是让组件 A 调用组件 B 的对象中的方法。但是,如果 A知道B的存在,他们是耦合的,A依赖于B,使得系统更难改变和维护。事件可以用来防止耦合

此外,作为使用事件和解耦组件的副作用,如果我们有一个团队只在组件 B 上工作,它可以改变组件 B 对组件 A 中的逻辑的反应方式,甚至无需与负责组件 A 的团队交谈。组件可以独立发展: 我们的应用程序变得更加有机

即使在同一个组件中,有时我们有一些代码需要作为操作的结果执行,但不需要在同一个请求/响应中立即执行。最明显的例子是发送电子邮件。在这种情况下,我们可以立即向用户返回响应并稍后以异步方式发送电子邮件,避免让用户等待电子邮件发送。

尽管如此,它还是有危险的。如果我们不分青红皂白地使用它,我们将面临最终逻辑流的风险,这些逻辑流在概念上高度内聚,但通过作为解耦机制的事件连接在一起。换句话说,应该在一起的代码将被分开,并且很难跟踪它的流程(有点像goto语句),理解它并推理它:这将是意大利面条代码!

为了防止我们的代码库变成一大堆意大利面条代码,我们应该将事件的使用限制在明确标识的情况下。根据我的经验,使用事件有以下三种情况:

解耦组件执行异步任务跟踪状态变化(审计日志)1.解耦组件

当组件 A 执行需要触发组件 B 逻辑的逻辑时,我们可以触发事件发送到事件分发器,而不是直接调用它。组件 B 将在调度程序中侦听该特定事件,并在事件发生时采取行动。

这意味着 A 和 B 都将依赖于调度程序和事件,但它们彼此不知道:它们将被解耦。

理想情况下,调度程序和事件都不应存在于任何组件中:

调度程序应该是一个完全独立于我们的应用程序的库,因此使用依赖管理系统安装在通用位置。在 PHP 世界中,这是使用Composer安装在vendor文件夹中的东西。然而,该事件是我们应用程序的一部分,但应该存在于两个组件之外,以使它们彼此不知情。该事件在组件之间共享,它是应用程序核心的一部分。事件是 DDD 所称的共享内核的一部分。这样,两个组件都将依赖于共享内核,但不会相互感知。

尽管如此,在 Monolithic 应用程序中,为了方便起见,将其放在触发事件的组件中是可以接受的。

共享内核

[…] 用明确的边界指定团队同意共享的域模型的某个子集。保持这个内核很小。[…] 这个明确分享的东西有特殊的地位,不应该在没有与其他团队协商的情况下改变。

Eric Evans 2014, 领域驱动设计参考

2.执行异步任务

有时我们有一段我们想要执行的逻辑,但执行起来可能需要相当长的时间,我们不想让用户等待它完成。在这种情况下,最好让它作为异步作业运行,并立即向用户返回一条消息,通知他他的请求将在稍后异步执行。

例如,在网上商店下订单可以同步完成,但发送电子邮件通知用户可以异步完成。

在这种情况下,我们可以做的是触发一个事件,该事件将被排队,并且会一直坐在队列中,直到工作人员可以在系统有资源时拿起它并执行它。

在这些情况下,关联逻辑是否在同一个限界上下文中并不重要,无论哪种方式,逻辑都是解耦的。

3.跟踪状态变化(审计日志)

在传统的数据存储方式中,我们让实体持有一些数据。当这些实体中的数据发生变化时,我们只需更新数据库表行以反映新值。

这里的问题是我们没有准确存储更改的内容和时间。

我们可以将包含更改的事件存储在审计日志类型的构造中。

更多关于这方面的信息,在关于事件溯源的解释中。

听众与订阅者

在实现事件驱动架构时,一个常见的争论是使用事件监听器还是事件订阅者,所以让我们澄清一下我的看法:

事件侦听器仅对一个事件作出反应,并且可以有多个方法对其作出反应。所以我们应该在事件名称之后命名侦听器,例如,如果我们有一个“ UserRegisteredEvent ”,我们将有一个“ UserRegisteredEventListener ”,这将使我们很容易知道,甚至无需查看文件内部,侦听器正在侦听什么事件. 对事件的方法(反应)应反映该方法实际执行的操作,例如“ notifyNewUserAboutHisAccount() ”和“ notifyAdminThatNewUserHasRegistered()”“。这应该是大多数情况下的常用方法,因为它使侦听器变小并专注于对特定事件做出反应的单一责任。此外,如果我们有一个组件化的架构,每个组件(如果需要的话)都会有自己的监听器来监听可以从多个位置触发的事件。事件订阅者对多个事件做出反应,并有多个方法对它们做出反应。订阅者的命名比较困难,不能ad-hoc,但是订阅者还是要遵守单一职责原则,所以订阅者的名字需要体现单一的意图。使用事件订阅者应该是一种不太常见的方法,尤其是在组件中,因为它很容易打破单一责任原则。事件订阅者的一个很好用例的例子是管理事务,更具体地说,我们可以有一个名为“ RequestTransactionSubscriber ”的事件订阅者对“ RequestReceivedEvent ”、“ ResponseSentEvent ”和“ KernelExceptionEvent ”等事件作出反应”,并分别将事务的启动、提交和回滚绑定到它们,每个事务都有自己的方法,如“ startTransaction() ”、“ finishTransaction() ”和“ rollbackTransaction() ”。这将是订阅者对多个事件做出反应,但仍然专注于管理请求事务的单一责任。图案

Martin Fowler 确定了三种不同类型的事件模式:

事件通知事件携带状态转移事件溯源

所有这些模式都共享相同的关键概念:

事件传达某事已经发生(它们发生 在某事之后);事件被广播到任何正在侦听的代码(多个代码单元可以对一个事件做出反应)。事件通知

假设我们有一个具有明确定义组件的应用程序核心。理想情况下,这些组件彼此完全解耦,但它们的某些功能需要执行其他组件中的某些逻辑

这是最典型的情况,前面已经介绍过:当组件A执行需要触发组件B逻辑的逻辑时,不是直接调用它,而是触发一个事件发送给事件派发器。组件 B 将在调度程序中侦听该特定事件,并在事件发生时采取行动。

需要注意的是,这种模式的一个特点是事件携带的数据最少。它只携带足够的数据让听众知道发生了什么并执行他们的代码,通常只是实体 ID,也许还有事件创建的日期和时间。

优点更大的弹性,如果事件被排队,原始组件可以执行它的逻辑,即使次要逻辑由于错误而无法在那个时刻执行(因为它们被排队,它们可以在错误被修复后执行);减少延迟,如果事件排队,用户不需要等待该逻辑被执行;团队可以独立地发展组件,使他们的工作更容易、更快、更不容易出现问题并且更有机;缺点如果在没有标准的情况下使用,它有可能将代码库变成一堆意大利面条代码。事件携带状态转移

让我们再次考虑前面具有明确定义的组件的应用程序核心示例。这一次,对于它们的某些功能,它们需要来自其他组件的数据。获取该数据的最自然方式是向其他组件询问它,但这意味着查询组件将知道被查询的组件:组件将相互耦合!

共享此数据的另一种方法是使用当拥有数据的组件更改数据时触发的事件。该事件将携带全新版本的数据。对该数据感兴趣的组件将监听这些事件,并通过存储该数据的本地副本来对它们做出反应。这样,当他们需要外部数据时,他们将在本地拥有它,而无需查询其他组件。

优点更大的弹性,因为如果查询组件变得不可用(因为存在错误或远程服务器无法访问),查询组件可以运行;减少延迟,因为不需要远程调用(当查询的组件是远程的)来访问数据;我们不必担心被查询组件的负载来满足所有查询组件的查询(特别是如果它是远程组件);缺点相同数据会有多个副本,尽管它们将是只读副本,并且现在数据存储不是问题;查询组件的复杂性更高,因为它需要逻辑来维护外部数据的本地副本,尽管这是非常标准的逻辑。

如果两个组件都在同一个进程中执行,则可能不需要这种模式,这有助于组件之间的快速通信,但即便如此,出于解耦和可维护性的考虑,或者作为将这些组件解耦为不同微服务的准备,使用它可能会很有趣,将来的某个时候。这完全取决于我们当前的需求、未来的需求以及我们希望/需要在脱钩方面走多远。

事件溯源

让我们假设一个处于初始状态的实体。作为一个实体,它有自己的身份,它是现实世界中的特定事物,应用程序正在对其进行建模。在其生命周期中,实体数据会发生变化,传统上,实体的当前状态只是作为一行存储在数据库中。

交易记录

这在大多数情况下都很好,但是如果我们需要知道实体如何达到该状态(即我们想知道我们银行账户的贷方和借方)会怎样?这是不可能的,因为我们只存储当前状态!

使用事件溯源,而不是存储实体状态,我们专注于存储实体状态更改并根据这些更改计算实体状态。每个状态更改都是一个事件,存储在事件流中(即 RDBMS 中的表)。当我们需要实体的当前状态时,我们从事件流中的所有事件中计算它。

事件存储成为事实的主要来源,系统状态完全来源于它。对于程序员来说,最好的例子就是版本控制系统。所有提交的日志是事件存储,源树的工作副本是系统状态。

Greg Young 2010, CQRS 文档

删除

如果我们有一个错误的状态变化(事件),我们不能简单地删除那个事件,因为那样会改变状态变化历史,而且它会违背进行事件溯源的整个想法。相反,我们在事件流中创建一个事件来反转我们想要删除的事件。这个过程称为逆转交易,它不仅将实体带回所需状态,而且还会留下一条痕迹,表明该对象在给定时间点曾处于该状态

不删除数据也有架构上的好处。存储系统成为一种仅添加的架构,众所周知,仅添加的架构比更新架构更容易分发,因为要处理的锁要少得多。

Greg Young 2010, CQRS 文档

快照

但是,当我们在事件流中有很多事件时,计算实体状态的成本会很高,而且性能不佳。为了解决这个问题,每发生 X 数量的事件,我们将在该时间点创建实体状态的快照。这样,当我们需要实体状态时,我们只需要计算到最后一个快照。该死,我们甚至可以保留一个永久更新的实体快照,这样我们就可以两全其美了。

预测

在事件溯源中,我们也有投影的概念,它是事件流中事件的计算,从特定时刻到特定时刻。这意味着快照或实体的当前状态符合投影的定义。但预测概念中最有价值的想法是,我们可以分析实体在特定时间段内的“行为”,这使我们能够对未来做出有根据的猜测(即,如果在过去 5 年中实体有如果 8 月的活动增加,明年 8 月很可能会发生同样的情况),这对企业来说可能是一种非常有价值的能力。

优点和缺点

事件溯源对业务和开发过程都非常有用:

我们查询这些事件,这对业务和开发都非常有用,可以了解用户和系统行为(调试);我们还可以使用事件日志来重建过去的状态,这对业务和开发同样有用;自动调整状态以应对追溯变化,非常适合业务;通过在重播时注入假设事件来探索替代历史,这对商业来说很棒。

但并非一切都是好消息,要注意隐藏的问题:

外部更新当我们的事件触发外部系统更新时,我们不想在重放事件时重新触发这些事件以创建投影。此时,我们可以在“重放模式”下简单地禁用外部更新,也许将该逻辑封装在网关中。

根据实际问题,另一种解决方案可能是缓冲对外部系统的更新,在一定时间后执行它们,此时可以安全地假设事件不会被重播。外部查询当我们的事件使用对外部系统的查询时,即。获得股票债券评级,当我们重播事件以创建预测时会发生什么?我们可能希望获得与第一次举办活动时相同的收视率,也许是几年前。因此,要么远程应用程序可以为我们提供这些值,要么我们需要将它们存储在我们的系统中,以便我们可以再次通过将该逻辑封装在网关中来模拟远程查询。代码更改Martin Fowler 确定了 3 种类型的代码更改:新功能、错误修复和时序逻辑。真正的问题出现在重播应该在不同时刻使用不同业务逻辑规则播放的事件时,即。去年的税收计算与今年不同。像往常一样,可以使用条件逻辑,但它会变得混乱,所以建议改用策略模式。

因此,我建议谨慎行事,并尽可能遵守以下规则:

让事件保持沉默,只知道状态变化而不是它是如何决定的。这样我们就可以安全地重播任何事件并期望结果是相同的,即使业务规则在此期间发生了变化(尽管我们需要保留遗留业务规则以便我们可以在重播过去的事件时应用它们);与外部系统的交互不应该依赖于这些事件,这样我们就可以安全地重播事件,而没有重新触发外部逻辑的危险,我们也不需要确保来自外部系统的回复与最初播放事件时的回复相同。

当然,就像任何其他模式一样,我们不需要在任何地方都使用它,我们应该在有意义的地方使用它,在它给我们带来优势并解决比它产生的问题更多的地方。

结论

同样,它主要是关于封装、低耦合和高内聚。

事件可以为代码库的可维护性、性能和增长带来巨大的好处,但是,通过事件源,还可以提高系统数据可以提供的可靠性和信息。

然而,这是一条有其自身危险的道路,因为概念和技术的复杂性都在增加,并且其中任何一个的误用都可能导致灾难性的后果。

参考资料:

标签: #php事件驱动