当我们在说事件驱动的时候,我们在说什么
Posted qfjavabd
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当我们在说事件驱动的时候,我们在说什么相关的知识,希望对你有一定的参考价值。
Martin Fowler是面向对象分析设计、重构等领域的顶级专家,也是敏捷开发的创始人之一,也是企业应用架构方面的顶级专家。
这篇文章的初衷,是在之前的ThoughtWorks开发者大会中,他们发现,一般人们在说到事件时,发现不同的人往往说的不是同一件事情。所以就有了这篇文章,将几种主要的事件模式整理出来,供大家参考。这样,以后大家再讨论事件启动架构的时候,可以先弄清楚对方讨论的是什么模式。
事件通知
这一模式就是一个系统发送一些事件消息到另一些系统,以通知他们说我这个系统里面的领域对象发生了改变。这个通知的一个关键点是,我的源系统并不关心对方系统收到这些通知以后的结果,甚至说,根据不期望有返回结果。如果说,在有些业务场景下,需要知道对方处理的结果的话,那也应该是通过另一个事件通知的逻辑来通知源系统。
所以,事件通知模式非常好的实现了系统之间的隔离性,而且很容易实现,我们只需要一个消息队列就能实现,随便一个开源的MQ服务器就能满足。
但是,这也有一个潜在的问题,就是事件通知的这个流(也就是一个事件从A系统发送到B系统,B系统处理完以后,又发送另一个事件到C系统),也是很难管理或监控的,因为它散落在各个系统的代码里。所以,当我们的系统变得复杂,系统(或者说服务)越来越多的时候,这些系统之间的事件及其流向,就很难维护了。但是,即便是这些弊端,事件通知模式还是很有用的,因为它非常简单。
使用事件通知还有一个需要注意的问题是,对事件的定义。事件是指的原系统发生了某件事情,导致领域对象的改变。但是很多人在使用的过程中,往往将事件和动作混淆。动作是发起请求的人希望目标系统执行的动作,而通知是源系统领域对象发生的事情。举个例子来说,A是订单服务,B是库存服务,当有一个订单时,要通知库存服务去更新库存。那么通知和命令是这样的:
通知:A通知B有了一个新订单,B自己根据业务决定如何根据订单处理库存)
命令:A命令B修改库存,这时候,A与B之间的关系就变了。
这在理解上很好理解,但是在实际开发中很容易忽略,需要引起注意。
有关事件通知,还有一点就是,事件不需要携带很多数据,而只是携带像id和对源数据的查询链接之类的数据。还是用上面的订单、库存为例,订单系统发出的事件中只有一个订单id和用来查询订单id的url,库存服务收到该事件以后,用这个url,加上订单id,获取订单的信息,这个信息是库存服务需要知道最少数据,如订单商品和数量。
个人认为,这种方式,虽然减少了消息队列中的数据传输,也减少了系统之间的数据结构的耦合性(目标系统需要知道原系统发出来的事件中的数据结构),但是,目标系统还是需要通过一定方式知道事件消息的数据是什么样的,而且每次还要重新取数据,可能原系统为了它这个请求还要多写一个接口,有点得不偿失。
携带状态的事件传递
这种模式跟上面的比,就是把目标系统处理事件时需要用到的数据都放在事件消息里。这种方式解决了上面说的一些问题。可能会带来的问题中,事件的存储量的增加应该算不上问题了。但是一个最大的问题就是,源系统和目标系统都需要知道事件中数据的结构。还有当事件中的数据结构发生改变,如增减字段等,那需要目标系统也做相应修改。这一点本来就是不可避免的,即使是上面的不携带数据的方式,目标系统也需要更新最新的数据结构获取信息。
事件溯源
事件溯源模式的核心思想是,在一个系统中,任何的状态的变化,都需要产生一个事件,并由这个事件触发相应业务状态的更改。在这种模式下,当前的业务状态,是由这些事件以及它们的处理方法生成的。如果我们将这些事件保存下来,在需要的时候,只要再重新触发这些事件,让这些事件的处理过程重新运行,就能够重新生成业务状态,甚至可以通过指定一个事件,来重新生成某一个时间点的数据状态。这也是一些人常说的历史重现。
有关事件溯源,有一个错误的认识是,事件溯源不一定非要是异步的。作者举了一个非常好的例子,是git资源库。一个git资源库可以看作是一个事件溯源的应用,我们提交的一个个commit就是一个个的事件,所有的commit都是依次、同步的作用在这个版本控制系统中,我们的资源库中的最新的文件,就是这些commit事件依次作用产生的结果。
对事件溯源的另一个误区是,在事件溯源系统中的每个请求,在处理这个事件的时候,不需要知道整个系统的所有事件,而只需要知道它自己感兴趣的那一类事件。还是以git资源库为例。git是一个分布式的版本管理系统,如果我编辑了资源库中的一个文件,修改完以后commit;这时另一个人在他的电脑也修改了这个资源库中的另一个文件,也commit到他的本地资源库。当我们两个人把这两个commit同步(push)到某个服务器上的资源库的时候,并不会冲突,git会根据我们的提交的时间,生成相应的commit。这时候服务器上的资源库里面,提交日志(相当于event log)里面看到的就是合并后的commit。
对于这个用git资源库来类比事件溯源,可以这么理解,每个人都可以把git资源库clone到本地,相当于将这个基于事件溯源的应用系统进行分布式部署。假设我们每个人在修改文件的时候,只允许修改一个文件,提交后才能修改另一个文件。每个提交的commit事件中都带有这个文件的id。这一个个的文件相当于事件溯源系统中的领域对象,如一个订单信息,一个商品信息,每个订单每个商品都有它的全局唯一ID。我们的每个事件就是在某一个领域对象上的更新操作,例如一个订单事件就是更新一条订单数据的状态。
在事件溯源系统中,领域对象的状态是根据跟他相关的事件生成的。也就是说,如果我们不保存业务状态数据,那么在每次处理一个订单事件的时候,都要取出这个订单的所有事件,然后根据这些事件生成当前的业务状态信息,然后再处理新的那个事件。
所以,系统在处理每个事件的时候,只需要获取这个事件所属的对象相关的事件,而不需要获取所有的事件。就好像我们在git中编辑一个文件,就先获取有关这个文件的commit记录,根据这些提交的commit事件,生成最新的文件,然后在这个文件上编辑,编辑完成后再提交一个新的commit事件。只不过,我们的git资源库会将本地的最新的文件状态保存下来,所以我们不需要每次都重新根据commit生成文件。在git中我们可以对某一个版本打tag,在事件溯源模式中,我们也可以用类似的方式创建快照,将领域对象的当前状态保存成snapshot快照,这样就不需要每次都获取所有相关事件重新生成业务状态了。
事件溯源模式有很多有意思的特性,比如我们可以将整个系统看做是一个有版本管理功能的业务系统。根据上面说的开始重现功能,我们可以将系统的业务状态充值到任何一个时间点。我们甚至可以在重现历史的过程中,添加一些假想的业务数据,用于进行一些业务验证。
当然,事件溯源模式也有一些问题,如上面说的历史重现,如果我们的系统需要依赖外部系统,那么该系统在重新历史的时候,其他的系统已经是最新的状态,这就时空错乱了,就会有错误。还有一个问题就是事件的数据结构的改变问题。如果事件的数据结构改变,但是历史的事件中,相应的数据就会缺失,或者多余。那么系统就需要既能处理历史版本的事件,也能处理新版本的事件。这无疑为我们的事件设计、和系统设计都带来一定的困难。
CQRS
Command Query Responsibility Segregation (CQRS)简单来说就是读写分离。也就是去操作使用的数据,跟写操作使用的数据不是同一个数据。虽然从根本上讲,实现CQRS模式,不一定要使用事件溯源模式来实现。但是一般情况下,当人们说CQRS的读写分离的时候,基本上都是通过事件溯源模式来实现的。
要实现CQRS模式,一般都是通过事件溯源模式来进行数据更新操作,也就是所有的业务数据状态的变更都通过事件来触发。然后,对于每个事件,领域对象处理完该事件的同时,还有另一个事件处理器,根据这个事件将最新的业务状态更新到数据库中。然后,对于所有的读操作,都从这个保存业务状态的数据库中获取。通过这种业务状态的数据和事件数据的分离,相互之间不会出现数据的锁,可以实现很高的吞吐量。
可以发现,CQRS模式,看起来很优雅,但是实现起来往往比较复杂,如果没有成熟的框架,而自己去实现,肯定会非常困难。
合理使用这些模式
至于说到如何合理的使用这些模式,首先要弄清楚这些模式。例如文中作者说到,有人说事件溯源模式给他们带来了灾难,每个事件都要处理两次,(一个是更新领域对象,一次是更新read model),实际上是因为他们把事件溯源跟CQRS搞混了。使用事件溯源模式,不一定要用读写分离的方式使用。还有人说系统中大量的异步通信,导致了大量的复杂性(有些时候我们需要一个事件的处理结果)。但是事件溯源模式不一定非要是异步发处理,这只是跟我们的实现有关,我们大可以在需要的地方使用同步的方式。
我个人觉得,事件溯源+CQRS模式,确实是非常的优雅,我之前用过一点Axon框架,可以用来实现事件溯源+CQRS模式,而且这个框架的设计也很清晰,使用起来也比较容易。但是,这种方式确实会增加代码量,因为一个事件需要有2个处理函数,分别更新业务状态数据和领域对象数据,还要定义一堆的命令和事件。除了Axon以外,也有一些别的框架,希望随着事件驱动架构的应用越来越多,相应的框架也会越来越多,越来越成熟。
?
以上是关于当我们在说事件驱动的时候,我们在说什么的主要内容,如果未能解决你的问题,请参考以下文章