一、什么是事件驱动架构
当下,随着微服务的衰亡,容器化技术的倒退,以及云原生、serverless 概念的遍及,事件驱动再次引起业界的宽泛关注。
所谓事件驱动的架构,也就是应用事件来实现跨多个服务的业务逻辑。事件驱动架构是一种设计利用的软件架构和模型,能够最大水平缩小耦合度,很好地扩大与适配不同类型的服务组件。在这一架构里,当有重要事件产生时,比方更新业务数据,某个服务会公布事件,其它服务则订阅这些事件;当某一服务接管到事件就能够执行本人的业务流程,更新业务数据,同时公布新的事件触发下一步。
事件的公布与订阅,须要依赖于一个牢靠的音讯代理。见下图:
当然,事实上有不少软件我的项目都应用了音讯队列,然而这里须要明确的是,对音讯队列的应用并不意味着你的我的项目就肯定是事件驱动架构,很多我的项目只是因为技术方面的驱动,小范畴地采纳了某些音讯队列的产品而已。偌大一个零碎,如果你的音讯队列只是用作邮件发送的告诉,那么这样零碎天然谈不上采纳了事件驱动架构。
在采纳事件驱动架构时,咱们须要思考业务的建模、事件的设计、上下文的边界以及更多技术方面的因素,这个系统工程应该如何从头到尾的落地,是须要通过思考和斟酌的。总而言之,“事件驱动架构”的设计并不是一件易事。本文在前面有个例子供参考。
另外,如果自觉应用事件驱动设计架构,就有可能要承当中断业务逻辑的危险,因为这些业务逻辑具备概念上的高度内聚,却采纳理解耦机制将它们分割在一起。换句话说,就是将本来须要组织在一起的代码强行拆散,并且这样难于定位解决流程,还有数据一致性保障等问题。为了避免咱们的代码变成一堆简单的逻辑,咱们该当在某些明确场景下应用事件驱动架构。以教训来讲,以下三 种场景能够应用事件驱动开发:
- 组件的解耦
- 执行异步工作
- 跟踪状态的变动
二、什么时候应用事件驱动架构
2.1 组件的解耦
当服务(或组件)A 须要执行服务 B 中的业务逻辑,相比于间接调用,咱们能够向事件代理(事件散发器)中发送一个事件。服务 B 通过监听散发器中的非凡事件类型,而后当这类事件被接管到时去执行它。
这意味着服务 A 和服务 B 都依赖于事件代理和事件,而无需关注彼此实现:即实现它们的解耦。见下图:
基于这种松耦合,服务能够用不同的语言实现。解耦后的服务可能轻松地在网络上互相独立地扩大,通过动静增加或删除事件生产者和消费者来批改他们的零碎,而不须要更改任何服务中的任何逻辑。
2.2 执行异步工作
有时咱们会有一系列须要执行的业务逻辑,然而因为它们须要消耗相当长的执行工夫,所以咱们不想看到用户消耗工夫去期待这些逻辑解决实现。在这种状况下,最好将它们作为异步工作来运行,并立刻向用户返回一条信息,告诉其稍后持续解决相干操作。
比方,内容字段的查看等入库流程能够采纳“同步”执行解决,然而执行内容了解则采纳”异步“工作去解决。在这种状况下,咱们所要做的是触发一个事件,将事件退出到工作队列中,直到一个服务可能获取并执行这个工作。此时,相干的业务逻辑是否处在同一个上下文中环境中并不重要,不管怎么说,业务逻辑都是被执行了。
2.3 跟踪状态的变动
在传统的数据存储形式中,咱们通过实体模型存数据。当这些实体模型中的数据发生变化时,咱们只需更新数据库中的行记录来示意新的值。这里有个问题,就是业务上咱们无奈精确存储数据的变更和批改工夫。然而在事件驱动架构中,能够通过事件溯源将蕴含批改的内容存入到事件里。上面会具体探讨“事件溯源“。
三、为什么应用事件驱动架构
当大家议论事件驱动架构时,比方大家说本人恰好在最近的我的项目中采纳了事件驱动架构,实际上,他们可能在议论上面这四种模式中的一种或者几种:
- 事件告诉
- 事件承载状态转移
- 事件溯源
- CQRS
注:概念起源 2017 年 GOTO Conference 上 Martin Fowler 分享的 The many meanings of Event-Driven architecture。
3.1 事件告诉
假如咱们当初想要设计一个繁难的内容平台,蕴含三局部:
- 内容引入零碎
- 作者微服务
- 关注核心
当内容创作者通过内容引入零碎上传视频之后,会触发如下的一个调用流程见下图:
- 内容引入零碎收到创作者上传的视频,执行入库流程;
- 内容引入零碎调用作者微服务的 API,减少“视频 - 创作者”的从属关系;
- 作者服务调用关注核心的 API,让关注核心给关注了这个创作者的其余用户发送作者视频更新的告诉。
下面这个调用流程,不可避免地创立了上面的依赖关系:
- 内容引入零碎依赖于作者微服务的 API,尽管内容引入零碎其实不太关怀作者微服务的业务。
- 作者微服务依赖于关注核心的 API,尽管作者微服务也不关怀关注核心的业务和解决流程。
这种依赖关系很有可能并不是咱们所冀望的。内容引入零碎是一个比拟通用的业务,不同类型的内容引入零碎很可能会有类似性能,如字段类型查看、入内容库、启动高敏审核等。作者服务则是一个十分业余的零碎,如不同源、不同类型的内容对于作者的业务逻辑是不同的。让一个通用的零碎依赖于一个业余的零碎,不论从设计角度,还是后续系统维护角度,都是不一个好的计划。作者微服务可能会常常依据业务需要做变更,但内容引入零碎绝对稳固,而下面这种依赖关系让咱们难以在“不对内容引入零碎做调整的状况”下随便更改作者微服务。
从架构层面,咱们心愿让作者微服务依赖于内容引入零碎,让一个业余的零碎依赖于一个稳固的、通用的零碎,减少零碎的稳定性。这个时候咱们能够借助于“事件告诉”。见下图:
长处
- 架构更强壮。如果退出队列的事件可能在源组件中执行,但在其它组件中因为 bug 导致其无奈执行(因为将其退出到队列工作中,它们能够在 bug 修复后再执行)。
- 业务解决缩小提早。当用户无需期待所有的逻辑都执行实现时,能够将这类工作退出到事件队列。
- 便于零碎扩大,可能让组件的研发团队独立开发,放慢我的项目进度、升高性能难度、缩小问题产生并且更有组织性。
- 将信息封装在“事件”里,便于零碎内流传。
毛病
- 如果没有正当应用,可能使咱们的代码变成“面条式”代码。
- 数据一致性问题。因为流程依赖于最终的一致性,因而通常不反对 ACID 事务,因而反复或乱序事件的解决会使服务代码更加简单,并且难以测试和调试所有状况。
“事件告诉”的毛病和长处绝对应,正是因为它提供了很好的解耦能力,咱们会比拟难通过浏览代码去失去整个零碎和流程的全貌。因为这些逻辑之间的关系不再是之前的依赖关系。这将会是一个挑战。
3.2 事件承载状态转移
咱们在应用事件告诉时,事件外面往往不会蕴含上游零碎解决这个事件须要的所有信息。比方当内容产生下架变更时,内容平台会生成一个“内容下架“的事件,但当上游零碎解决这个事件时,往往还须要晓得,该内容上个状态是什么,是谁触发下架等信息,能力实现后续解决。所以不可避免地,上游零碎在解决这个事件时,往往还须要通过平台服务来获取这些额定信息。
为了解决这个问题,咱们引入一个种新的模式,叫做“事件承载状态转移”。简略来说,就是让事件的生产方本人保留一份在业务处理过程中须要用到的上游零碎的数据。比方让上游零碎保留一份在解决内容状态变更事件时所须要用到的内容变更前的状态,防止回头去平台查问。
长处
- 架构更强壮。缩小事件生产方对生产方的额定依赖(获取事件处理所需数据);
- 业务解决缩小提早。减少事件生产方零碎的响应速度,因为不再须要调用平台 API 以获取事件处理所需数据;
- 无需放心被查问组件的负载(尤其是近程组件)。
毛病
- 只管当初数据存储曾经不再是问题本源,仍然会保留多个只读的数据正本,一致性进一步被毁坏;
- 减少数据处理的复杂度,即便解决逻辑符合规范,它也须要额定解决和保护内部数据的本地正本业务逻辑。
3.3 事件溯源
有些时候咱们岂但关怀零碎以后的状态,咱们还关怀如何变成以后这个状态的,然而数据库仅仅简略地保留实体的以后状态。事件溯源能够帮忙咱们解决这个问题。
事件溯源是一个特地的思路,它并不长久化实体对象,而是只把初始状态和每次变更的事件记录下来,并在内存中依据事件还原实体对象的最新状态,mysql 主从备份用到的 binary log 以及 redis 的 aof 长久化机制,都能够认为是“事件溯源”的实现。
事件溯源在做完数据库更新之后,它将事件的发送操作转换为往数据库或者日志零碎中写入一条事件记录,其它节点通过查询数据库或者文件系统,来失去这些事件,并通过回放来确保数据的最终一致性。
长处
- 能够出现一个残缺的变动历史;
- 提供更不便的 debug 伎俩;
- 能够回溯到任何一个历史状态;
- 不便批改以后事件;
毛病
- 要实现一个牢靠和高性能的事件仓库(保留的事件记录)并不是一件容易的事件,利用代码须要依据事件库的 API 进行重写。
3.4 CQRS
CQRS 全称是 Command Query Responsibility Segregation。简略来说,就是针对零碎的读写操作,应用不同的数据模型、API 接口、平安机制等,来达到对读写操作的齐全隔离,满足不同的业务需要。见下图:
依据存储在事件库中的事件汇合,能够计算失去每个业务实体的状态,这些状态以物化视图的形式存储在一个数据库中。当有新的事件产生时,也同样会自动更新视图。这样,视图查问服务就能够像查问一般的数据库数据一样实现各种查问场景。具体的设计可参考下图所示:
四、事件驱动架构在内容平台中的实际
在当今社会,内容“横行”的时代,内容平台企业须要有极强的灵活性和应变能力。特地是在中国这样一个内容行业(如视频)飞速发展的市场里,企业要求平台可能疾速地对内容业务需要做出应答,否则就会丢失先发劣势。这有点相似于现代战争条件下,各国都要求部队具备快速反应能力,这种能力次要体现在平台可能通过疾速开发或者重用 / 整合现有资源来达到疾速响应业务需要。
随着内容行业业务越来越宏大简单,所波及的存储类型、处理器、账号体系、效率工具、数据和结算零碎等十分多,这就要求平台有很强的整合能力以及对异构环境的适配能力。
最初,因为内容行业的倒退突飞猛进,特定类型的内容业务(如小视频)都会在其初中期倒退后迎来一个疾速收缩期,业务量和业务类型会急剧减少,这也要求平台有很好的可扩展性。相干平台架构见下图:
4.1 创立事件
事件其实是 DDD(畛域驱动设计)中的一个概念,示意的是在一个畛域中所产生的一次对业务有价值的事件,落到技术层面就是任何影响业务流程或者状态的扭转。事件具备本人的属性,比方产生的工夫、产生了什么、事件之间的关系、状态以及变动,事件也能够生成新的事件,依据不同的事件生成新的业务事件。在创立事件时,首先须要记录事件的一些通用信息,比方惟一标识 ID 和创立工夫等,为此创立事件基类 ContentEvent:
public abstract class AbstractContentEvent {
private String eventId;
private String publisher;
private String receiver;
private Long publishTime;
}public abstract class AbstractContentEvent {private String eventId; private String publisher; private String receiver; private Long publishTime;}
在个别场景下,事件个别随着聚合根(也是 DDD 的一个概念,这里泛指视频 id)状态的更新而产生,另外,在事件的生产方,有时咱们心愿监听产生在某个聚合根下的所有事件,为此倡议为每一个聚合根对象创立相应的事件基类,其中蕴含聚合根 videoId,比方对于视频(Video)类,创立 VideoEvent:
public class VideoEvent extends AbstractContentEvent {private final String videoId;}
而后对于理论的视频事件,对立继承自 VideoEvent,比方对于视频引入的 VideoInputEvent 事件;
public class VideoInputEvent extends VideoEvent {private Article article; // 视频根本信息}
视频域事件的继承链见下图;
在创立事件时,须要留神两点:
- 事件自身应该是不变的;
- 事件应该携带与事件产生时相干的上下文数据信息,然而并不是整个聚合根的状态数据。例如,在视频引入时能够携带视频的根本信息 article,而对于视频状态更新的 VideoStatusChangeEvent 事件,则应该同时蕴含更新前后的状态 status:
public class VideoStatusChangeEvent extends VideoEvent {
private String preStatus; // 更新前的状态
private String status; // 更新后的状态
}
4.2 公布事件
公布事件有多种形式,比方能够在应用程序中公布。通常的业务处理过程都会更新数据库而后公布事件,这里一个比拟常见的场景是:须要保障数据库更新和事件公布之间的原子性,也即要么二者都胜利,要么都失败;当然也有不须要保障原子性的场景。如果须要保障原子性,以“内容引入”的业务流程为例,见下图:
- 接管内容;
- 写入内容表;
- 写入事件表,且和内容表的更新在同一个本地数据库事务中;
- 事务实现后,触发事件的发送;
- 读取事件表;
- 将事件发送到音讯队列;
- 发送胜利后,将记录标注为“已发送”;
4.3 生产事件
在生产事件时,除了实现根本的音讯解决逻辑外,咱们须要重点关注以下三点:
- 生产方的幂等性;
- 生产方有可能进一步产生事件;
- 生产方的数据一致性;
对于“幂等性”,事件的发送机制保障的是“至多一次投递”,这是有消息中间件保障,技术选型时须要留神。为了可能正确地解决反复音讯,要求生产方是幂等的,即屡次生产事件与单次生产该事件的成果雷同。保障“生产幂等性”的办法有很多,这里介绍一种。在生产方创立一个事件表,用于记录曾经生产过的事件,在处理事件时,首先查看该事件是否曾经被生产过,如果是则不做任何生产解决。
对于第二点,仍然沿用前文讲到的“事件表”的形式。事实上,无论是解决服务申请,还是作为音讯的生产方,对于聚合根(videoId)来讲都是无感知的,事件由聚合根产生进而由事件库长久化,这些过程都与具体的业务操作源头无关。
对于“数据一致性”,实质上是由第二点引出,事件驱动架构在业务对象之间通过异步的音讯来同步状态,有些音讯也能够同时公布给多个服务,在“音讯引起了一个服务的同步”后可能会引起另外的音讯,事件会扩散开。严格意义上的事件驱动是没有同步调用的,如何保障一致性,就要比非事件驱动架构要简单,通常采纳“cache aside”模式和“分布式锁”来保障一致性。
综上,在生产事件的过程中,应用程序须要更新业务表、事件记录表,此时整个事件的公布和生产过程见下图;
五、总结
支流场景下,传统面向服务(或以数据驱动)的平台存在系统性有余,须要加强以下能力:
- 在传统数据集成根底上须要进一步晋升业务集成能力。
- 须要进步集成平台的业务敏捷性和反馈能力。
- 须要进一步实现业务零碎间的解耦和高可靠性。
- 须要进一步晋升管控平台的实时响应能力。
”事件驱动架构“人造地满足了这些能力要求。事件驱动架构”天生“的长处,比方,封装、高内聚和低耦合,还能够晋升代码的可维护性、性能和业务增长的需要,通过事件溯源模式,还能进步零碎数据的可靠性。
不过,事件驱动同样存在弊病,因为无论是概念上的复杂度还是技术上的复杂度都减少了,当它被滥用时将导致灾难性的结果。所以,在技术栈的选用方面,给出以下寄语:
1)不要“自觉的追新”技术人员的爱好往往是什么技术风行就追什么技术。当初的技术倒退快,前后端不断涌现各种框架,咱们巴不得把这些框架都用在本人的我的项目里才行,按理论登程,按需所用,适当的预留技术预研的空间。
2)不要“按技术站队,以后果反推“很多人把伎俩当成了目标,成为了框架的信徒。用了 Java 开发,你的设计就肯定是面向对象吗?用了 Spring boot 就是微服务了吗?肯定要技术和理论场景联合,架构师也要深刻理解把握技术,然而更多的是理解技术的优劣和应用场景,而不是简略的生吞活剥。
作者:vivo 互联网服务器团队 -Gao Xiang