1.什么是事件驱动架构目前,随着微服务的兴起,容器技术的发展,以及云原生和serverless概念的普及,事件驱动架构再次引起了业界的广泛关注。所谓事件驱动架构就是利用事件实现跨多个服务的业务逻辑。事件驱动架构是一种设计应用程序的软件架构和模型,可以最大限度地降低耦合度,并能很好地扩展和适应不同类型的服务组件。在这个架构中,当一个重要事件发生时,比如更新业务数据,一个服务会发布这个事件,其他服务会订阅这些事件;当一个服务收到事件后,它可以执行自己的业务流程并更新业务数据,同时发布一个新的事件来触发下一步。事件的发布和订阅需要依赖可靠的消息代理。见下图:当然,其实很多软件项目都使用了消息队列,但是这里需要明确的是,使用消息队列并不意味着你的项目就一定是事件驱动架构。在方面的驱动下,一些消息队列产品只是在小范围内使用。这么大的系统,如果你的消息队列仅仅用于邮件通知,那么这样的系统还不能说采用了事件驱动架构。在采用事件驱动架构时,我们需要考虑业务建模、事件设计、上下文边界以及更多技术因素。这个系统工程应该如何从头到尾实施,需要思考和推敲。总而言之,“事件驱动架构”的设计并不是一件容易的事。本文后面有例子供参考。另外,如果一味地使用事件驱动的设计架构,可能要冒着打断业务逻辑的风险,因为这些业务逻辑具有高度的概念内聚性,而是使用解耦机制将它们绑在一起。换句话说,就是把需要组织在一起的代码强行分离,处理流程很难定位,同时也存在数据一致性保证等问题。为了不让我们的代码变成一堆复杂的逻辑,我们应该在某些明确的场景下使用事件驱动架构。从经验来看,事件驱动开发可用于以下三种场景:组件解耦异步任务执行跟踪状态变化2.何时使用事件驱动架构1.组件解耦当一个服务(或组件)A需要executeaservice对于B中的业务逻辑,相比直接调用,我们可以向事件代理(eventdispatcher)发送一个事件。服务B侦听特定事件类型的调度程序,然后在收到此类事件时执行它。这意味着服务A和服务B都依赖于事件代理和事件,而不关心彼此的实现:它们的解耦已经完成。见下图:基于这种松耦合,服务可以用不同的语言来实现。解耦服务可以轻松地在网络上相互独立地扩展,通过动态添加或删除事件生产者和消费者来修改它们的系统,而无需更改任何服务中的任何逻辑。2.执行异步任务有时候我们有一系列的业务逻辑需要执行,但是因为它们执行的时间很长,我们不希望看到用户花时间等待这些逻辑过程完成。在这种情况下,最好将它们作为异步任务运行并立即向用户返回一条消息以便稍后继续处理。例如,内容字段的检查等入库过程可以使用“同步”执行来处理,但内容理解的执行则使用“异步”任务来处理。在这种情况下,我们所要做的就是触发一个事件,该事件将事件排队,直到服务可以获取并执行任务。此时,不管相关的业务逻辑是否在同一个上下文中,反正业务逻辑都执行了。3.跟踪状态变化在传统的数据存储方式中,我们通过实体模型来存储数据。当这些实体模型中的数据发生变化时,我们只需更新数据库中的行记录以表示新值。这里有个问题,就是我们在业务中不能准确的存储数据的变化和修改时间。但是在事件驱动的架构中,修改的内容可以通过事件追踪的方式存储在事件中。下面将详细讨论“事件溯源”。3.为什么要使用事件驱动架构当你谈到事件驱动架构时,比如你说你最近的项目中刚刚采用了事件驱动架构。事实上,他们可能在谈论以下四种模式中的一种或多种:EventcarryingstatetransfereventtraceabilityCQRS注:概念来自MartinFowler在2017年GOTO大会上分享的ThemanymeaningsofEvent-Drivenarchitecture1.事件通知假设我们现在要设计一个简单的内容平台,它由三部分组成:内容导入系统作者微服务焦点中心当内容创作者通过内容导入系统上传视频时,会触发下面的调用流程,如图下图:内容导入系统接收创作者上传的视频并执行存储过程;内容导入系统调用作者微服务的API,增加“视频-创作者”的隶属关系;作者服务调用关注中心的API,让关注中心给关注该作者的其他用户发送更新该作者视频的通知。上面的调用过程不可避免地会产生如下依赖:内容导入系统依赖作者微服务的API,虽然内容导入系统不太关心作者微服务的业务。作者微服务依赖于焦点中心的API,虽然作者微服务并不关心焦点中心的业务和处理流程。这种依赖很可能不是我们所期望的。内容导入系统是一个比较常见的业务,不同类型的内容导入系统可能具有相似的功能,例如字段类型检查、内容库录入、高敏感审核激活等。作者服务是一个非常专业的系统。例如,不同来源和不同类型的内容对作者来说具有不同的业务逻辑。让一个通用的系统依赖于一个专业的系统,无论从设计还是后续系统维护的角度来看,都不是一个好的解决方案。作者微服务可能会根据业务需求频繁变更,但内容导入系统相对稳定,上述依赖性让我们很难“不对内容导入系统做调整”,随意更改作者微服务。从架构层面,我们希望作者微服务依赖内容导入系统,让专业的系统依赖稳定的通用系统,增加系统的稳定性。这时候,我们可以使用“事件通知”。见下图:1)优势架构更健壮。如果入队事件在源组件中可以执行,但在其他组件中由于bug导致无法执行(因为入队到队列任务中,修复bug后才能执行)。业务处理减少延迟。当用户不需要等待所有逻辑执行完毕时,可以将此类工作添加到事件队列中。方便系统扩展,让组件研发团队自主开发,加快项目进度,降低功能难度,减少问题,更有条理。将信息封装在“事件”中,以便于在系统内传播。2)缺点如果使用不合理,我们的代码可能会变成“面条状”的代码。数据一致性问题。由于流程依赖于最终一致性,一般不支持ACID事务,因此重复或乱序事件的处理会使服务代码复杂化,并且难以测试和调试所有情况。“事件通知”的缺点与优点相对应。也正是因为它提供了很好的解耦能力,让我们更难通过阅读代码来获得整个系统和流程的全貌。因为这些逻辑之间的关系已经不是之前的依赖关系了。这将是一个挑战。2.事件携带状态传递当我们使用事件通知时,事件往往不包含下游系统处理事件所需要的所有信息。例如,当内容下架时,内容平台会产生“内容下架”事件,但下游系统在处理该事件时,往往需要知道该内容的上次??状态是什么,谁触发了移除等信息以供后续处理。因此,下游系统在处理这个事件时,难免往往需要通过平台服务获取这些附加信息。为了解决这个问题,我们引入了一种新的模式,叫做“事件携带状态转移”。简单来说就是让事件的消费者保留一份业务流程中需要用到的上游系统的数据。比如让下游系统在处理内容状态变化事件时需要用到内容变化前的状态副本,避免返回到平台查询。1)优势架构更健壮。减少事件消费者对生产者的额外依赖(获取事件处理所需的数据);业务处理减少延迟。提高事件消费者系统的响应速度,因为不再需要调用平台API来获取事件处理所需的数据;无需担心被查询组件(尤其是远程组件)的负载。2)缺点虽然数据存储不再是问题的根源,但仍然会保存多个只读数据副本,一致性会进一步被破坏;会增加数据处理的复杂度,即使处理逻辑符合规范,也需要额外处理并维护一份外部数据业务逻辑的本地副本。3.事件溯源有时候我们不仅仅关心系统当前状态,我们还关心如何变成当前状态,但是数据库只是简单的保存了实体的当前状态。事件溯源可以帮助我们解决这个问题。事件溯源是一种特殊的思维方式。它不持久化实体对象,只记录初始状态和每次变化的事件,并根据事件恢复内存中实体对象的最新状态。mysql主从备份使用了二进制日志和redis的aof持久化机制,都可以认为是“事件追踪”的实现。事件源更新完数据库后,将事件发送操作转化为向数据库或日志系统中写入一条事件记录,其他节点通过查询数据库或文件系统获取这些事件,并通过以下方式保证数据的完整性回放。最终一致性。1)优势可以呈现完整的变更历史;提供更方便的调试手段;可以回到任何历史状态;方便修改时事;2)缺点需要实现一个可靠且高性能的事件库(保存事件记录)并不是一件容易的事,需要根据事件库的API重写应用代码。4、CQRS的全称是CommandQueryResponsibilitySegregation。简单来说,就是针对系统的读写操作,通过不同的数据模型、API接口、安全机制等,实现读写操作的完全隔离,满足不同的业务需求。见下图:根据事件库中存储的事件集合,可以计算出各个业务实体的状态,并将这些状态以物化视图的形式存储在数据库中。当有新的事件产生时,视图也会自动更新。这样,视图查询服务就可以像查询普通数据库数据一样,实现各种查询场景。具体设计可以参考下图所示:4.事件驱动架构在内容平台的实践当今社会,在内容“横行”的时代,内容平台企业需要具备很强的灵活性和适应性。尤其是在中国这样一个内容产业(如视频)快速发展的市场,企业需要平台能够快速响应内容业务需求,否则将失去先发优势。这有点类似于现代战争的条件。各国都要求其军队具备快速反应能力。这种能力主要体现在平台通过快速开发或复用/整合现有资源来快速响应业务需求的能力。随着内容行业的业务越来越大、越来越复杂,涉及到的存储类型、处理器、账户系统、效率工具、数据和结算系统等众多,这就要求平台具备强大的集成能力,适应异构环境。适应性。最后,由于内容产业的快速发展,特定类型的内容服务(如小视频)在经过初期和中期发展后将迎来快速扩张期,业务量和业务类型将急剧增加,这也要求平台具有良好的可扩展性。相关平台架构如下图所示:1.创建事件事件实际上是DDD(DomainDrivenDesign)中的一个概念,意思是在某个领域发生的有价值的业务事件,对技术上的任何影响等级。业务流程或状态的变化。事件有自己的属性,比如发生的时间、发生了什么、事件之间的关系、状态、变化等。事件也可以产生新的事件,可以根据不同的事件产生新的业务事件。创建事件时,首先需要记录事件的一些通用信息,如唯一ID、创建时间等,为此创建一个事件基类ContentEvent:publicabstractclassAbstractContentEvent{privateStringeventId;私有字符串发布者;私有字符串接收器;私有长发布时间;}一般场景下,事件一般随着聚合根状态的更新而产生(也是DDD的一个概念,这里泛指videoid)。另外,在事件的消费者端,有时我们希望监听发生在某个聚合根下的所有事件,建议为每个聚合根对象创建一个对应的事件基类,其中包含聚合根视频编号。例如,对于视频(Video)类,创建一个VideoEvent:publicclassVideoEventextendsAbstractContentEvent{privatefinalStringvideoId;然后对于实际的视频事件,统一继承自VideoEvent,比如视频引入的VideoInputEvent事件;publicclassVideoInputEventextendsVideoEvent{privateArticle文章;//视频的基本信息}视频领域事件的继承链如下图所示;创建事件时需要注意两点:事件本身要保持不变;事件应该携带与事件发生相关的上下文数据信息,而不是整个聚合根的状态数据。比如导入视频时,可以携带视频文章的基本信息,对于视频状态更新的VideoStatusChangeEvent事件,还应该包含更新前后的状态:publicclassVideoStatusChangeEventextendsVideoEvent{private字符串预状态;//更新前的状态privateStringstatus;//更新状态}2.发布事件发布事件的方式有很多种,比如可以在应用中发布。通常的业务流程会更新数据库,然后发布事件。这里一个常见的场景是:需要保证数据库更新和事件发布之间的原子性,即要么都成功,要么都失败;当然,没必要保证Atomic场景。如果需要保证原子性,以“内容引入”业务流程为例,如下图:接收内容;写内容表;写入事件表,并在同一个本地数据库事务中更新内容表;交易完成后,触发事件的发送;阅读事件表;将事件发送到消息队列;发送成功后,将记录标记为“已发送”;3、在消费事件时,除了完成基本的消息处理逻辑外,我们还需要关注以下三点:消费者的幂等性;消费者可能产生更多事件的可能性;消费者的数据一致性;对于“幂等性”,事件发送机制保证“至少一次传递”,这是由消息中间件来保证的,需要注意技术的选型。为了正确处理重复的消息,要求消费者是幂等的,即多次消费一个事件和消费一次事件的效果是一样的。保证“消费者幂等性”的方法有很多种,这里介绍一种。在消费端创建事件表,记录消费过的事件。在处理一个事件时,首先检查该事件是否已经被消费,如果是,则不做任何消费处理。对于第二点,还是沿用了上面说的“事件表”的方法。事实上,无论是处理服务请求,还是作为消息的消费者,对聚合根(videoId)都是不敏感的。事件由聚合根生成,然后由事件库持久化。这些过程与特定的业务操作有关。来源无关紧要。对于“数据一致性”,本质上是从第二点衍生出来的。事件驱动架构通过异步消息同步业务对象之间的状态。一些消息也可以同时发布到多个服务。在“一个消息引起一个服务同步”中可能会引起额外的消息,并且事件会传播开来。严格意义上的事件驱动是没有同步调用的。如何保证一致性比非事件驱动的架构要复杂。通常使用“cacheaside”模式和“分布式锁”来保证一致性。综上所述,应用在消费事件的过程中,需要更新业务表和事件记录表。此时整个事件发布和消费流程如下图所示;5.在主流场景下,传统的面向服务(或数据驱动)平台存在系统性的不足,需要增强以下能力:在传统数据集成的基础上,业务集成能力需要进一步提升改善。需要提高集成平台的业务敏捷性和响应能力。需要进一步实现业务系统之间的解耦和高可靠。管控平台实时响应能力有待进一步提升。“事件驱动架构”自然满足了这些能力需求。事件驱动架构的封装性、高内聚、低耦合等“天然”优势,也可以提高代码的可维护性、性能和业务增长需求。通过事件源模型,还可以提高系统数据的可靠性。但是,事件驱动也有缺点,因为无论是概念上的复杂度还是技术上的复杂度都增加了,一旦被滥用,将会导致灾难性的后果。因此,在技术栈选择方面,给出如下建议:1)不要“盲目追求新技术”技术人员倾向于追求流行的技术。现在的技术发展日新月异,前后端各种框架不断涌现。我们迫不及待地想在自己的项目中使用这些框架,从实际出发,按需使用,适当预留技术预研空间。2)不要“和技术站在一起,和结果站在一起”很多人把手段当成了目的,成了框架的信徒。Java开发,你的设计一定要面向对象吗?使用SpringBoot是微服务吗?需要将技术与实际场景相结合,架构师也需要对技术有深入的了解,但更多的是了解技术的优缺点和使用场景,而不是简单地生搬硬套。
