事件溯源模式(Event Sourcing Pattern)

此文翻译自msdn,侵删。

原文地址:https://msdn.microsoft.com/en-us/library/dn589792.aspx

 

本文介绍了一种有利于物化(materialize)领域内的模型数据的持久化方式,这种方式记录领域中已经发生的所有一系列事件,而不是仅仅记录数据最终的状态。这种模式通过避免同步数据模型和业务领域来简化领域中的复杂业务;增强了系统的性能,可伸缩性和响应性;保证了业务数据的一致性;并且可以支持审计流程和历史回溯来实现修正操作(compensating actions)。

 

实际情况和问题

大多数的应用都和数据打交道,最常见的打交道方式就是将用户在使用过程中的数据最终状态同步到数据库中。例如,在传统的增删改查(CURD)模式中,一个典型的数据过程就是从数据库中读出数据,修改完后再把修改后的数据更新到数据库中——通常来说,在这个更新过程这张数据表是被锁住的。

这种传统的增删改查(CURD)方式存在一些局限性:

  • 事实上执行这种直接依赖数据库的增删改查(CRUD)开销会影响系统的性能和响应性,不利于系统的可伸缩性。
  • 在一个存在多个用户并发操作的领域中,因为多个用户也许会同时操作同一张表,所以数据更新造成的冲突更加可能发生。
  • 除非系统额外有一个可以记录所有业务细节的日志系统以实现审查机制,否则所有的历史都会丢失。

 

解决方案

事件溯源模式针对数据操作会产生的问题定义了一套解决方案:它只往一个事件数据库中增加数据操作过程中经历过的事件。应用程序代码会将数据操作过程中经历的所有动作描述成事件,然后持久化到事件数据库中。每一个事件都描述了数据的一系列变化(例如”往订单中添加商品“)

数据的当下的状态以事件的形式储存在扮演“source of truth”或者是“system of record”(数据或者信息的可信赖的数据源)角色的数据库中。事件数据库会发布这些事件,用户可以得到通知,如果有需要的话还可以获取这些事件。用户可以在其他的系统中使用这些事件去初始化一些任务,或者添加新的必要的事件去完成任务。需要注意的是,生成事件的程序代码和订阅事件的系统是独立无耦合关系的。

在应用中改变实体的时候,事件数据库发布的事件常用来维护物化视图,或者通过与外部系统结合来完成某些任务。例如一个系统可能维护一个顾客订单的物化视图,这个视图是某个用户界面要呈现的一部分。当应用程序添加新的订单,或者往订单里面添加、删除东西,或者添加货运信息,可以获得这些描述这些变化的事件然后更新物化视图。

在任何时候通过事件溯源的方式都可以让应用程序获取所有的历史事件,然后使用这些历史事件,通过高效“回播”来物化现在的状态。这种情况可能在处理一个物化视图请求的时候发生,或者发生在用一个既定的事件来将现阶段的实体以物化视图的方式储存起来以支持展现层的特定需求的时候。

图1展示了这种模式的一个概览,包含了一些使用事件流的例子,比如与外部应用或者系统事件的结合创建物化视图和通过重现事件来创建某个实体的当前状态。

image

图1

一个事件溯源模式的概览和例子

 

事件溯源模式有许多优点,包括:

  • 事件是不变的,所以可以用只增加的方式去保存。用户界面,工作流或者初始化产生事件的过程就都可以不被干扰地进行,因为处理这些事件的工作任务可以在后台运行。而且,事务在执行时候不会造成冲突,这样就可以极大地改善应用程序的性能和可伸缩性,特别是对于用户展现层。
  • 事件是描述已经发生的事件的简单实体,事件同时也包含一些描述事件的数据。 事件不会直接地把数据库中的数据更新掉,他们只是简单地被储存起来以备使用。这些事件使用和维护起来非常简单。
  • 事件对领域专家有特殊的意义,然而可能领域专家没有明白数据库中各个表而导致复杂的实体关系被错误地映射了。数据表只是表现了系统的当前状况,但不是已经发生的事件。
  • 事件溯源可以防止并发更新造成的冲突因为这种方式防止操作直接去更新数据库中的记录。然而,领域模型必须被设计得能够防止不一致状态的发生。
  • 只增的事件数据存储方式为监视对数据库的操作提供了审查的途径,在任何时候都能通过重现事件的方式以实现物化视图或者规划,并且可以帮助检查和测试系统。并且,一些撤销操作的修正操作可以通过进行历史操作的反向操作来实现,而这个在一个仅仅记录当前状态的系统中是无法实现的。事件的列表也可以用来分析应用程序的性能情况和查看用户的行为趋势,或者用来挖取其他的商业信息。
  • 事件和任务的解耦保证了系统的灵活性和扩展性。例如,一些处理事件的任务仅仅需要考虑事件数据库发布的事件本身属性和描述事件的数据。这种执行任务的方式和引发事件的操作是低耦合的。并且,多个任务都可以处理各自的事件。这样的话方便与其他的服务和系统的合作,只需要监听事件数据库发布的事件就可以了。然而,溯源的事件是在一个很低层次上,有的时候需要产生一系列事件。

 

一些问题和考虑

当决定如何应用这个模式的时候,应该思考以下几点:

  • 当创建物化视图或者产生数据规划的时候,系统只会保证数据的最终一致性 。在应用程序向事件数据库中添加处理请求的事件,和事件的发布,和事件的使用者三者之间有一些延迟。在这个延迟的期间,更多新的改变的事件也许会产生并且添加到事件数据库中。
  • 事件存储是不变的信息,所以事件数据永远不能被更新修改。唯一一种撤销修改的操作是添加一笔反向操作的事件到数据库中,就像会计中的反向交易一样。如果已经持久化的事件数据的格式(而不是数据本身)需要修改,这就很难把现有的事件和新版本的事件融合起来。这可能必须得遍历所有的事件然后改变他们,再和新事件融合起来,或者创建旧事件的新版本也行。可以考虑使用版本戳(version stamp)来记录不同的事件概要来维护无论是新的还是旧的版本的事件格式。
  • 多线程的应用和多实例的应用可能保存在事件在事件数据库中。事件数据库中事件的一致连续性是非常重要的,就像一系列顺序的事件影响一个特定的实体(这个事件影响实体的顺序影响了这个实体现在的状态)。为每一个事件加一个时间戳是一个预防这种问题产生的方式。其他的方式就是使用一个增长的标识来标注每一个事件。如果一个动作尝试着去同时为一个同样的实体添加同样的事件。事件数据库可以拒绝一个和现存实体标识符和事件标识符匹配的事件。
  • 并没有标准的方法或者固定的机制,例如使用SQL查询,去事件 数据库中获得事件信息。唯一的能获取到的数据是一系列用标识符为标准的的事件。一个事件id都会对应一些各自的实体。 实体的当前状态只能通过重播所有从开始到现在发生的事件获取。
  • 当管理和更新系统的时候,事件的长度可能会造成一些问题。 如果事件序列的长度过长,可以考虑通过每经过一定次数的事件进行一个快照。实体的当前状态可以通过获得一个实体的快照,然后把快照事件之后发生的所有事件重播。
  • 尽管事件溯源能够将更新数据的冲突的可能性最小化,但是应用程序必须能够解决最终一致性和事务缺失产生的不一致问题。例如,当一个货物的订单正在生成的时候,一个库存减少的事件可能刚刚进入数据库, 导致要去协调两个操作,可能是通知客户或者生成一个反向的订单。
  • 事件的发布可能是“至少一次”,所以事件消费者应该是幂等的。They must not reapply the update described in an event if the event is handled more than once. For example, if multiple instances of a consumer maintain an aggregate of a property of some entity, such as the total number of orders placed, only one must succeed in incrementing the aggregate when an “order placed” event occurs. While this is not an intrinsic characteristic of event sourcing, it is the usual implementation decision.(抱歉这段我实在看不懂到底在说什么,特别是an aggregate of a property of some entity,一些实体的一个属性的聚合??说实话这篇文章里面有很多地方英文和中文都不通顺,大概是我水平不行吧。)

 

什么时候使用这种模式

这种模式在以下几种场景中是最理想的解决方案:

  • 当你想获得数据的“意图”,“目的”或者“原因”的时候。 例如,一个客户的实体改变可能用一系列的类似于”搬家“,”注销账户“或者”死亡“等事件类型。
  • 并发更新数据时候非常需要减少或者完全避免冲突的时候。
  • 当你需要保存已经发生的事件,并且能够重播他们来还原到某个状态、使用这些事件去回滚系统的某些变化或者仅仅是历史或者审查记录的时候。例如 ,当一个任务包括几个步骤,你可能需要执行一个撤销更新的操作然后重播过去的每个步骤来回到稳定的状态。
  • 当使用事件是一些应用程序的某些操作的天然属性,并且需要很少的额外扩展或者实施的时候。
  • 当你需要把插入,更新数据和需要执行这些操作的应用程序解耦开的时候。用这种模式可以提高UI的性能,或者把这些事件分发给其他的监听者,比如有些应用程序或系统,它们在一些事件发生的时候必须做出一些反应。例如,将一个工资系统和一个报销系统结合起来,这样的话当报销系统更新一个事件给事件数据库,数据库对此做出的相应事件就可以被报销系统和工资系统共享。
  • 当要求变更或者——当和CQRS配合使用的时候——你需要适配一个读的模型或者视图来显示数据,而你想要更灵活地改变物化视图的格式和实体数据的时候。
  • 当和CQRS配合使用的时候,并且当一个读模型被更新时能接受数据的最终一致性问题,或者说从一系列的事件序列中生成实体对性能的影响可以被接受。

这种模式在以下几种场景中可能并不适用:

  • 小而简单的,业务逻辑简单或者根本没有业务逻辑,或者领域概念的,一般传统的增删改查(CURD)就能实现功能的业务领域,或者系统。
  • 需要实时一致和实时更新数据的系统。
  • 不需要审查,历史和回滚的系统。
  • 并发更新数据可能性非常小的系统。例如,只增加数据不更新数据的系统。

 

例子

一个会议管理系统需要追溯已经预订会议室人数,这样当一个人去预订会议室的时候就可以查看是否还有座位。这个系统可以至少用一下两种方式去保存预订的总数:

  • 这个系统可以保存关于预订总数的信息并且以一个在预订信息数据库中分离的表来存储。当用户增加或者取消预订的时候,这个系统就可以增加或者减少这个预订的总数。这种方式理论上来讲很简单,但是这个可能会造成一些可伸缩性的问题,比如在一个很短的时间内有大量的会议参与者预订座位。例如在预订结束的最后或者前一天。
  • 这个系统可以把预订和取消预订当作事件保存在一个事件数据库中。然后系统就可以根据通过重播这些事件来获取作为预订的数量。由于事件的独立性,这个系统的伸缩性更强。这个系统只要能够实现从事件数据库中读取事件,或者把事件数据添加到事件数据库中的功能。关于新增或者取消预订的事件是不能修改的。

图2展示了会议系统的座位预订子系统通过事件溯源模式的实现。

image

图2

在一个会议管理系统中使用事件溯源来获得关于座位预订的信息

预订两个座位的操作顺序是:

  1. 用户界面发出一个为两个参会者预订座位的请求。这个请求被一个独立的事件handler(一个用来处理请求和用户界面分离的逻辑)处理。
  2. 通过获得预订和取消预订的事件来构建一个包含所有会议座位预订信息的聚合。这个聚合被称为SeatAvailability,被包含在领域模型中,这个领域模型向外暴露获得和新增聚合信息的方法。
  3. 这个处理请求的handler通过调用领域模型暴露出来的方法来完成预订操作。
  4. 这个SeatAvailability的聚合记录了一个包含已经被预订的座位数量的事件。下一次这个聚合就可以通过计算所有预订和取消预订事件来计算还有多少座位剩下。
  5. 系统向事件数据库增加事件。

如果一个用户想要取消座位,系统可以通过执行一个类似的步骤,不同之处在于这个处理事件的handler通过处理一个取消座位的请求然后在事件数据库中添加一个取消的事件。

使用事件溯源模式不仅仅提供了更大的伸缩性,还提供了一个预订和取消预订一个会议的完整的历史。这些事件数据库中的事件是明确的,仅有的source of truth。因为系统可以方便地回播所有事件以保存所有的历史状态,所以没有必要去将聚合持久化,因为系统可以通过回播事件来获得任意时候的状态。

相关模式和指南

当应用这个模式的时候以下模式和指南可能与之有关:

  • 读写分离(CQRS)模式 为CQRS实现的独立信息来源的写的储存常常是基于事件溯源模式。读写分离模式介绍了如何去把读数据和写数据分离。
  • 物化视图模式 系统的数据库的设计一般对高效查询不友好。一个常见的解决方法就是在一定的时间内或者当数据产生变化的时候去生成一个预先已经完成的查询视图。物化视图模式就是介绍这种方法的实现。
  • 事务修正模式 在事件数据库中的数据使不能修改的,新的事件进入来提供实体新的状态。要想撤销变更,要使用修正路口因为简单地去撤销之前的改变是不可能的。事务修正模式介绍了如何去撤销之前的操作。
  • 数据一致基础 当使用事件溯源模式和一个分离的读或者物化视图,读到的数据并不是实时一致的而是最终一致。数据一致基础总结了这些围绕保证数据发布一致性的问题。
  • 数据分表指南 当使用事件溯源模式的时候,为了保证系统的可伸缩性,减少冲突并且增强性能,数据通常是被分表的。数据分表指南介绍了如何将数据分表和可能会产生的问题。
posted @ 2016-09-04 14:55  balavatasky  阅读(6284)  评论(1编辑  收藏  举报