Event Sourcing也叫事件溯源,是这些年另一个越来越流行的概念,是大神Martin Fowler提出的一种架构模式。简单来说,它有几个特点:
- 整个系统以事件为驱动,所有业务都由事件驱动来完成。
- 事件是一等公民,系统的数据以事件为基础,事件要保存在某种存储上。
业务数据只是一些由事件产生的视图,不一定要保存到数据库中。
这么说可能还是比较难以理解,我们来举个栗子。这是一个账户余额管理的例子:
在这个图中,中间的是我们的账户对象,它有几个时间处理函数create()
, deposit()
, withdraw()
,分别用于处理新建账户、账户存款和取款的操作。
左边的就是一个个的事件,它是一个事件的流,根据用户请求或者从其他地方产生。在这里例子当中,有3个事件:AccountCreated
, AccountDeposited
, AccountWithdrawed
,分别相当于账户创建的事件,存款的事件和取款的事件。当这些事件产生的时候,我们会触发上面的Account对象的相应的处理函数。
右边的就是这个Account对象处理完左边的4个事件以后,最新的数据状态。具体的处理过程就是:
- 系统产生一个新建账户的事件
AccountCreated
,Account
对象来处理这个事件,事件里面的Id是1234,系统先尝试着找id是1234的账户,发现没有,于是新建一个Account
对象,并在它上面调用create()
的处理函数,也就是初始化了id和余额。- 然后,有一个
AccountDeposited
的事件,对应的Account
对象的id是1234,系统找到之前创建的对象,在它上面应用deposit()
处理函数,也就是增加的余额的操作,更新了账户余额。- 系统又收到一个存款事件,跟上面一样,又更新了一次余额。
- 收到一个取款事件,还是找到id是1234的账户,在它上面调用
withdraw()
的处理函数,进行取款操作,更新余额。- 最后,1234这个账户的余额是200,也就是右边的数据状态。
同时,上面的这些事件需要持久保存在数据库或其他地方,而account的数据状态却不需要保存,我们只是在需要获得account当前的数据状态的时候,通过这个account相关的事件,调用他们的处理函数,重新生成当前状态。当然,每次都这样调用处理函数势必会造成资源的浪费,因为它需要从数据库中取得所有这个account的事件,然后依次调用处理函数。所以一般我们可以把这个account的最新状态,以一种视图的方式保存在数据库中。
上面这个方式和过程,就是我们说的Event Sourcing,也就是以事件为源的处理模式。
通过上面的例子,我们理解了Event Sourcing(事件溯源),下面我们再来看看Event Sourcing包含哪些部分。
在上面的例子中,Account
对象就是一个聚合对象,它里面包含账户的基本信息,也包含了对账户操作时的处理方法,也就是几个事件处理函数。了解领域驱动设计的人这时候应该就想到,这个Account对象其实就是一个领域模型,Account这个领域模型需要的业务操作,由它自己提供。
每个聚合对象都有一个Id,用于唯一标识这个对象,所以系统中不同的账户就会有不同的Account对象。
我们说了,在Event Sourcing模式当中,所有的事件都是要保存到数据库(或其他存储,下面就直接说数据库了)中的,这个存储就叫Event Store。
每个事件应该也包含一个它要处理的聚合对象的id,以及事件的顺序,查询的时候就是根据聚合对象的id从数据库中找到相关的事件,并按照生成的事件或序号排序。
Event Store除了提供事件数据的存储、查询功能以外,还可以提供事件的重现等功能。事件的重现,就是将截止某一个时间的所有事件取出来,调用他的处理函数,生成当时那个时间点的业务状态。所以在重现之前,如果我们的业务数据的状态,通过视图的形式保存到了数据库中,我们需要先清除相应的数据。正是由于Event Sourcing模式的这个以事件为源的特性,所以我们才有可能提供这样的历史重现的功能。
一般情况下,我们的聚合对象的数据状态是不会保存在数据库当中的。每当系统要获得某一个账户的数据的时候,都是从Event Store当中取出所有相关聚合对象的事件,然后依次的调用这些事件的处理方法,“聚合”出该领域对象最新的数据状态。这个,就是聚合资源库需要提供的功能。
上面我们也说了,如果每次都重新“聚合”出对象,获取当前的状态,会浪费很多资源。所以,我们可以在某个事件发生的时候,将这个聚合对象的最新数据状态,写到一个表中,这个表可以叫做物化视图。
由于我们提供了专门的视图表,将聚合对象的最新状态保存在数据库中,那我们在查询的时候,可以通过该物化视图去查询,而不是通过聚合对象的资源库去查询。
CQRS,是 Command Query Responsibility Segregation的缩写,也就是通常所说的读写隔离。在上面,我们说,为了性能考虑,将聚合对象的数据状态用物化视图的形式保存,可以用于数据的查询操作,也就是我们把数据的更新与查询的流程隔离开来。我们通过事件来更新聚合对象的数据状态,同时由另一个处理器处理相同的事件,来更新物化视图的数据。
所以,Event Sourcing与CQRS有着天然的联系,所以也经常会有人把他们放在一起讨论。实际上,CQRS是在使用Event Sourcing模式以后,又使用了物化视图的情况下,所产生的额外的好处。
下图就是使用Event Sourcing好CQRS模式以后的一个简单的流程图:
- 对于Command类型的请求(需要修改数据),web层会走通过Event Sourcing更新聚合对象的流程,这时会有一个Event Handler的处理类监听相应事件,更新物化视图。
- 对于Query类型的请求,web层会通过相应的DAO获取数据返回。
Event Sourcing之所以会越来越受到关注,是因为它的一些优点:
当然,它也有一些缺点:
如果要开发一个基于Event Sourcing模式的分布式系统,最简单的方式就是用2个服务分别提供读和写的功能。写服务接收Command请求,触发聚合对象上的处理函数,更新聚合数据。然后把这个事件发送到一个MQ队列上。读服务监听这个队列,获取事件,更新相应的物化视图的数据。同时所有的Query请求都由读服务处理并返回。
对于写服务,它的数据只有事件,是一个流式的写操作,还有基于索引的查询。对于读服务,我们又可以部署多个应用,进一步提供数据查询的性能。可以看到,通过这么一个简单的读写分离,我们就能大大提高系统的性能。
使用Event Sourcing有它的优点也有缺点,那么什么时候该使用Event Sourcing模式呢?
- 首先是系统类型,如果你的系统有大量的CRUD,也就是增删改查类型的业务,那么就不适合使用Event Sourcing模式。Event Sourcing模式比较适用于有复杂业务的应用系统。
- 如果你或你的团队里面有DDD(领域驱动设计)相关的人员,那么你应该优先考虑使用Event Sourcing。
- 如果对你的系统来说,业务数据产生的过程比结果更重要,或者说更有意义,那就应该使用Event Sourcing。你可以使用Event Sourcing的事件数据来分析数据产生的过程,解决bug,也可以用来分析用户的行为。
- 如果你需要系统提供业务状态的历史版本,例如一个内容管理系统,如果我想针对内容实现版本管理,版本回退等操作,那就应该使用Event Sourcing。
链接:http://www.imooc.com/article/40858