什么是事件溯源Event Sourcing?

转载:http://www.jdon.com/48501
本文比较全面以易懂方式阐述了什么是事件溯源以及优缺点。
什么是Event Sourcing?“传统”保存应用程序变化数据的方式是存储当前状态。
例如,您的应用程序可能是一个日历,所以您想存储约会。它可能看起来像这样:约会ID 开始时间 结束时间 标题
1 09:30 10:45 会议
2 11:15 11:30 饭局

这是一张看起来很熟悉的数据表。它可能是关系数据库中的一个表,或一个键值存储中的一组文档,甚至一个存储在内存中的对象列表。重点是,它代表了系统的状态,因为它是现在。让我问你一个问题:这个系统是如何进入这个状态的?审计日志audit log要回答这个问题,您可以创建一个审计日志。除了记录创建或更新约会,在审计日志中您还将记录描述所发生的事件(一个“事件”)。对于一个单一的约会,它可能看起来像这样:
Sequence 事件Event1
约会被创建:时间:09:30 - 10:30
Title: 开会2
约会重新被安排: 09:30 - 10:453
标题被改变: 饭局

相比如果你只有系统的当前状态,这揭示了一个大量的信息。在约会被创建一次,约会改期一次,它的标题被改变了。10:30是预约结束时间,但它后来被改期,之前的预约信息会消失。这种信息对应用程序的用户来说是非常有用的,因为他们可以看到发生了什么事,他们应该责怪谁。开发人员也将从中获利最多;当你看到为什么导致某个特定的状态时,寻找问题的原因就变得容易得多了。单一真理之源除了你的当前状态以外存在一个审计日志是有用的,但有一个问题:冲突。如果你发现自己在目前的状态A,而你的审计日志说是B,你现在有两个问题。你不仅要诊断手头的问题,而且你也必须找出其中的差异原因。问题是,你有两个“真相”的来源,陈述你系统的状态应该是什么样的。您的应用程序只能查看到当前状态,所以它没有任何问题。你作为一个开发人员面对两个来源的真相。因为他们冲突,也就都没有真正值得信任,如同一个人戴两只表,如果时间不一样,都无法信任了。事件溯源如果我们消除审计日志,我们只有一个真相的来源,但我们失去了所有详细的历史信息,这些信息我们非常重视。如果我们消除了当前的状态呢?这是事件溯源的本质:它不是存储系统的当前状态,您只存储导致该状态的事件。为了获得当前状态,您可以在内存中“重播”这些事件。当前状态只是发生了事件的“短暂”表示。它不是恒久不变的。你可以改变你的应用程序的状态,但你不能改变事件。他们已经成为不争的事实。重播为了得到您的系统的当前状态,您将必须重播所有的事件。所有的事件?嗯,不是真的都是。您当前的状态通常被分为几个逻辑的“对象”,这将是传统上一个表中的行。如果您唯一标识每个对象,您只需要重播该对象的事件,以获得该对象的状态。让我们来看看一个非常简单的例子,我们的日历目前的状态。
class Appointment{public Guid AppointmentId { get; }public DateTime StartTime { get; private set; }public DateTime EndTime { get; private set; }public string Title { get; private set; }public bool IsCanceled { get; private set; }}

事件看上去如下:class AppointmentCreated{public Guid AppointmentId { get; }public DateTimeOffset StartTime { get; }public DateTimeOffset EndTime { get; }public string Title { get; }}class AppointmentRescheduled{public DateTimeOffset StartTime { get; }public DateTimeOffset EndTime { get; }}class AppointmentRenamed{public string Title { get; }}class AppointmentCanceled{}

AppointmentCanceled 事件没有任何属性。它的类型代表发生了一个有意义的事件。从约会视角重播这些事件看起来像什么呢?void ReplayEvent(AppointmentCreated @event){AppointmentId = @event.AppointmentId;StartTime = @event.StartTime;EndTime = @event.EndTime;Title = @event.Title;}void ReplayEvent(AppointmentRescheduled @event){StartTime = @event.StartTime;EndTime = @event.EndTime;}void ReplayEvent(AppointmentRenamed @event){Title = @event.Title;}void ReplayEvent(AppointmentCanceled @event){IsCanceled = true;}

你开始一个空白对象。然后为每个发生的事件简单调用ReplayEvent,按照它们发生的顺序。这是所有您需要重新创建当前状态的步骤。修改状态因为状态是代表过去发生的事件,修改状态就是通过添加事件实现。获得当前状态是对象的一种职责责任,它方便让所有的业务逻辑放在一个地方,包括业务规则的知识,它是DDD中的哲学。当前状态的对象(或实体对象或领域对象,不管你怎么称呼它)也会对自己负责一致性;它是唯一的创造事件的角色,也负责从过去的事件重播决定是否允许或禁止某个操作:你的约会已经取消;你就不能再取消了。class Appointment{public Appointment(Guid id,DateTimeOffset startTime,DateTimeOffset endTime,string title){if(endTime < startTime)throw new EndTimeBeforeStartTimeException();AppendEvent(new AppointmentCreated(id, startTime, endTime, title));}public void Reschedule(DateTimeOffset startTime,DateTimeOffset endTime){AppendEvent(new AppointmentRescheduled(startTime, endTime));}public void Cancel(){if (IsCanceled)throw new AppointmentAlreadyCanceledException();AppendEvent(new AppointmentCanceled());}}

AppendEvent 是一个方法,该方法将为指定的唯一对象也就是约会Appointment代表的对象添加一个事件到存储中。在方法内完成参数的验证。它使得一种方法最终负责数据的有效性和一致性。为简洁起见,Reschedule 不验证参数,但Cancel根据目前的状态检查操作是否允许。当前,如果您试图取消已被取消的约会,将引发异常。通常不会这样。我们可以跳过检查,并只是存储事件。它不会改变已被取消的事实;当重播时,我们还将设置iscanceled属性为true时。这里的要点是:你不必总是需要维护和基于状态验证。有时它会忽略多余的事件。注意,所有的构造器和Reschedule或Cancel 的方法都是存储事件。他们并没有直接修改状态。为什么?我们已经有了一个基于事件修改状态的方法:ReplayEvent方法。所以,除了存储事件,AppendEvent也将立即重播的事件。数据表事件的规则特点:1. 事件描述了已经发生的事情,因为我们不能改变过去,他们是不可变的。在他们持久后,事件不能改变。2.扩展以前的规则:他们不能被删除。3.每一个事件都包含表示状态变化所需的所有信息,并能够重播它。如果他们可以来自其他事件,他们不应该包括计算值。4.事件的元数据包含它的类型,当它发生时间5.您不能查询事件。它们只用于重放一个给定的时间内系统的状态。对于一个事件源对象,一个更改状态的请求只会导致三个东西中的一个:1.存储事件;2.抛出一个例外(因为一个业务规则会被侵犯);3.什么都不做。其他的结果,如直接修改对象的状态或进行数据库调用,都是非常令人沮丧的,因为它们是副作用。副作用通常不会被存储的事件表示,这意味着你不能重播它们。也使测试更难。好处因为你只需要读取和附加事件,存储是非常容易的。你需要做的唯一的事情是添加一个事件和检索事件列表。你可以存储事件在任何地方:在一个表中,一个键值存储,一个文件目录,一个电子邮件,或几乎任何你可以想象的地方。由于事件是不可变的,他们是非常容易缓存,这使您能够获得优异的性能。您将不再需要显式的事务,因为您只需要插入数据,而不需要更新或删除它;您也只需要一个表、集合、set,或是您的数据存储组数据。事务就像一把锁,所以不需要事务是一种像无锁代码:没有死锁和性能快得多。您还得到一个免费的审计日志,您可以在调试时使用,看看到底发生了什么,为什么系统处于一个特定的状态。如果您确定实体类中的操作是无副作用得,它基本上生活在隔离中,类Class将有一个非常低的耦合度量。你已经从系统的其余部分中分离出了业务逻辑。如果你已经实现了这些分离,测试你的逻辑可以表示为“指定的这些事件发生了,当这个操作被请求,那么这些事件是否被存储?”或者,“那么这个异常会被抛出吗?”你不必处理建立在代数上的关系数据库,因为你的对象不是。事件溯源回避了对象关系阻抗匹配。你的真理之源是一个数据库中的一堆事件。在你存储它们之后,你也可以通过一个消息系统广播到世界各地。它很容易让其他应用程序知道在你的系统中发生了什么。您不再需要到另一个系统存储中查询数据,然后复制以找出发生了什么变化。所有您需要存储的是:一个唯一对象标识符的列表和他们的最新已知的版本号。缺点当然,每一个方法的缺点,事件溯源也不例外。最重要的实际缺点是:没有简单的方法来查看您的应用程序的持久状态是什么,你不能通过查询得知。只有当你只能看到运行时发生了什么时。一个解决方案,是使用一个单独的模型读取发生在您的系统中的事件,由此需要CQRS。事件溯源是一个完全不同于大多数人都习惯的方法。您需要定义可以在您的系统中发生的每一个事件,所以添加新的功能可能会比过去习惯的慢。为了弥补这一差距,你的数据变得更珍贵。事件溯源听起来像是一个非常低效的机制来存储状态。它比当前状态直接存储需要更多的存储空间。它还需要更多的处理时间,仅仅是因为需要检索更多的数据才能到达当前状态。因为你现在拥有几乎无限量的存储和处理能力,克服这些不足是很容易的。其中之一是使用快照。快照你可能会认为,当一个事件流包含数千或数万的事件时,您的系统必须变得缓慢,因为每一个操作需要重播所有这些事件。嗯,不一定。记住,唯一做回放事件是由于要改变有状态。它应该是幂等;无论你重播事件一次或一百次,它应该有相同的结局。我们重播以后,比如一万事件,我们可以得到结果状态的快照并存储,以最后一个重播事件为标识。现在,当我们想要当前状态时,我们只需加载快照和回放快照以后任何发生的事件,如果你已经正确创建了你的快照,为了重放所有的事件加载快照和重放新的事件应该是相同。测试前面提到过测试变得容易多了。让我们更具体一点,展示如何测试一个事件溯源对象。private AppointmentCreated CreateAppointmentCreatedEvent(){return new AppointmentCreated(appointmentId: Guid.NewGuid(),startTime: DateTimeOffset.Now.AddHours(3),endTime: DateTimeOffset.Now.AddHours(4),title:"Appointment"
);}private AppointmentRenamed CreateAppointmentRenamedEvent(){return new AppointmentRenamed(title:
"Renamed appointment"
);}private Appointment CreateSut(IEnumerable<IEvent> events){var sut = new Appointment();foreach (var @event in events)sut.ReplayEvent(@event);return sut;}[Fact]public void Reschedule_AppendsAppointmentRescheduled(){
// GIVEN these events have happened
var events = new IEvent[]{CreateAppointmentCreatedEvent(),CreateAppointmentRenamedEvent()};var sut = CreateSut(events);
// WHEN we ask to reschedule
var newStartTime = DateTimeOffset.Now.AddHours(5);var newEndTime = DateTimeOffset.Now.AddHours(6);sut.Reschedule(newStartTime, newEndTime);
// THEN does the AppointmentRescheduledEvent get published?
sut.AppendedEvents.ShouldAllBeEquivalentTo(new[]{new AppointmentRescheduled(newStartTime, newEndTime)},config => config.RespectingRuntimeTypes());}

请注意,测试可以分为三个逻辑部分:给定的一些状态,什么时候一个动作执行,那么我们可以观察到这种行为吗?这种测试正式的语言为:Cucumber 。类比事件溯源是非常强大的思维方式,但它不真的是新的。我们大多数人都是熟悉的系统中都存在它。一些例子:1.您的银行帐户可能存储使用事件溯源。当前账户余额只是你所做的所有存款和取款的最终结果。2.版本控制系统。每个提交或更改到文件都是一个事件。如果您重播所有的事件,您将得到源代码的当前状态。3.大多数大型RDBM关系数据库内部使用事件溯源。简单地说,只有三个事件:插入,更新和删除。RDBMS存储事件在事务日志中并将其运用到数据表操作上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,597评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,805评论 0 11
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,365评论 0 17
  • 今早,接到妹妹的短信,告诉我二大爷家的大哥昨天突然去世。看到消息的一刹那,心里咯噔一下,难以相信这突如其来的死...
    怒放的叶子阅读 1,236评论 4 3