对LMAX架构的新的理解,让自己对event sourcing的做了更多的思考

最近又学习了一下LMAX架构。

让我对该架构以及event sourcing模式又有了很多新的认识和疑问。

LMAX architecture:input event + business logic processor(BLP) + output event

架构主要执行过程:
首先input event由上层(如controller)创建并最后统一汇集到input disruptor(一个并发控制组件),然后BLP在单个线程内处理所有的input event,一般处理的情况有:1)简单时,直接让aggregate 处理,处理完之后aggregate会产生output event;2)如果是复杂的过程,如long running process,则通过saga的方式来控制整个业务流程;saga接收input event,调用相关的aggregate来处理input event,同理,aggregate产生相应的output event;实际上我把saga也看成是一种聚合,因为saga也有状态,saga表达了一个流程的处理状态,saga也有唯一标识,saga也需要被持久化;总之,BLP在处理完input event后会产生output event。然后这些output event会被某些关心的event handler处理;另外有些event handler在处理output event时又会产生另外的input event并最终也发送到input disruptor,整个过程大概是这样。不知我理解的是否正确。

下面针对我上面的理解再做一些总结:

1. 整个过程有下面这几个主要元素构成:input event + BLP(包含aggregate,saga) + output event
2. input event,output event用于消息(message)传递,实际上他们都属于消息,并且也都是domain event?
3. BLP用于处理业务逻辑(由aggregate负责)和流程控制逻辑(由saga负责);
4. aggregate产生output event,output event会最终被发送到output disruptor;
5. output event有两个主要作用:1)可以让领域外知道领域内发生了什么;2)可以通过output event串联某些复杂的业务过程,如银行转账,如提交订单,etc;
6. 值得注意的是:整个BLP(saga+aggregate)是in-memory的,重建BLP是用input event来实现,而不是output event;这也是为什么LMAX架构中在BLP处理input event之前必须先通过一个叫journaler的东西持久化input event的原因。目的就是为了在需要的时候利用这些input event通过event sourcing(事件溯源)模式重建整个BLP。其实这个行为更直白的理解就是让BLP再重新处理一遍所有的input event;当然,在重建过程中对于任何要访问外部系统接口的地方,都要禁止访问,否则会带来问题,尤其是更新外部系统的时候,这个其实比较简单,只要设计一个gateway即可,重建blp的时候设置一下该gateway即可。

接下来我想阐述一些我觉得自己比较纠结的地方:

event sourcing的中文解释是事件溯源,关键是如何理解溯源?我的理解是:根据已经发生的事情来重现历史。如果这个理解是正确的,那何为已经发生的事情?lmax是通过input event来溯源,也就是说Lmax认为已经发生的事情是input event,而非output event,即LMAX认为已经发生是指只要input event一旦被创建就表示事情已经发生了,即已经发生是针对用户而言的,如用户提交了订单,那就是OrderSubmitted,用户点击了修改资料的保存按钮,那就是UserProfileChangeRequested;而我们之前的做法是根据aggregate产生的output event来溯源,即我们认为已经发生是相对aggregate而言的;那么到底哪种思路更好呢?虽然两种做法都能最终还原BLP。但就我个人理解,我觉得lmax的做法更合理,实际上如果让LMAX和CQRS架构的command端做对比,那么input event相当于command,只不过command一般都是动词,所以就是CreateOrder,ChangeUserProfile。所以可以理解为lmax架构实际上是在replay command;所以问题就演变为我们到底应该replay command还是replay event?想想replay是谁在replay?是聚合根,这点毫无疑问。另外,replay从语义上来说实际上就是和play做的事情是一样的,只不过是“重做”的意思。那么要理解重做首先要理解什么是“做”?我对“做”的理解就是执行行为并改变状态。所以“重做”就是重新重新执行行为并改变状态;replay command相当于是在重做别人给aggregate一些命令;而replay event相当于是在重做aggregate自己以前曾今做过的一些事情。其实,最重要的一点是,到底要重做什么?是重做用户的要求(what user want to do)还是重做聚合根内已经发生的事情(what domain has happened.),这个问题的回答直接决定到底该replay command 还是 replay event,呵呵。所以,按照这样的思路来思考就很明显了,LMAX是在重做用户的要求,而我们之前的replay event则是在重做聚合根内已经发生的事情。如果我认为重做应该是重做用户的要求,那replay event就不是真正意义上的重做了,而仅仅只是改变状态。举例:假设有一个Note聚合根,有一个ChangeTitle的公共方法,然后还有一个ChangeTitleCommand,ChangeTitleCommand的handler会调用Note的ChangeTitle方法;另外Note还有一个OnTitleChanged的私有方法,用于响应TitleChanged事件。如果是replay command,那会导致ChangeTitle会被重新调用,这就是重做用户的要求;而如果是replay event,则只有OnTitleChanged方法被重新调用,也就是说只是在重做聚合根内已经发生的事情。思考到这里,我不得不承认第一个思考出这种思路的人很厉害,因为他用了这种绕个弯的做法(将本来可以放在一个方法内一次性完成的任务(先改状态然后再产生output event))拆分为两个步骤,第一步是先仅仅产生一个TitleChanged的事件,第二步才是响应该事件并作出状态改变。这样拆分的目的是可以让第二步的方法(OnTitleChanged方法)可以用于event sourcing。另外,这两步对聚合根外部来说是透明的,因为外部根本不知道内部是通过两个步骤来实现的。不得不承认这种做法在replay的时候远比replay command要容易的多,因为所有的aggregate的内部事件响应函数都不会涉及与任何外部系统的交互。虽然这种做法挺好,但是我觉得我们非常有必要搞清楚这两种不同的event sourcing的区别。

另一方面,从确保event必须被持久化的角度来讲:我觉得LMAX的架构,即replay command的好处是,可以很容易在进入BLP之前持久化command,真正做到在BLP处理之前确保所有事件已经被持久化了;而如果是replay event,那我们就没办法实现一个in-memory的BLP了,因为首先BLP是in-memory的,即没有任何IO,但是我们又要求必须持久化output event。那怎么办呢?如果是同步的方式持久化output event,那就不是in-memory了;如果是异步的方式来持久化output event,那虽然可以做到in-memory,但怎么确保output event一定已经被持久化了呢?
[该贴被tangxuehua于2013-02-16 23:19修改过]
[该贴被tangxuehua于2013-02-16 23:36修改过]

2013-02-17 09:11 "@banq"的内容
ES和DE的区别 ...

这个链接的内容跟我所表达的内容关系不大呀。

这个链接的内容和我表达的内容不是同一回事呀

后来又做了一些思考。

想来想去,最终还是倾向于应该通过output event来做event sourcing。因为毕竟只有output event才真正表示domain aggregate认可的可以发生的domain event。而我们要重建的就是聚合根,到底是应该通过重复执行用户的命令来让模型达到最新状态还是通过让聚合根重新执行已经发生过的事情呢?现在想来,应该是后者。虽然前者也可以,但是要付出的代价相对比较大,比如重建时要禁用外部系统的调用,最麻烦的还是重启发布时的很多细节问题要考虑;而通过聚合根已经发生的事情来重建,则相对很容易,因为重建时不会涉及任何模型之外的东西!

但是因为我们现在采用了in-memory domain的架构,所以传统的基于数据库事务的做法已经无法使用了。所以需要设计另外一种架构确保在domain修改状态之前domain event已经被持久化了,为什么要做这个保证是因为event sourcing+in memory的架构实际上是一种event driven architecture,即整个领域模型的状态的修改都是由事件驱动的,这意味着如果要改变内存中的领域模型的状态,那必须先确保引起该状态修改的domain event必须已经被持久化了。

从用户发起一个command后执行的流程如下:disruptor是并发控制组件,大家可以暂时理解为一个消息队列。如果要进一步了解disruptor,可以看看LMAX架构。

1.Send ChangeNoteTitleCommand to input disruptor;
2.Command handler execute method called by input disruptor;
3.Note.ChangeTitle method called by command handler execute method;
4.NoteTitleChanged domain event is created and raised in note.ChangeTitle method;
5.The infrastructure framework send the above NoteTitleChanged event to input disruptor when the raise method is called;
6.Journal event handler called by input disruptor to persist the event;
7.Another event handler called by input disruptor to really apply all the note state changes according with the event.
8.A third event handler called by input disruptor to send the event to the output disruptor;
9.All the external event handlers are called by the output disruptor; for example, some external event handlers will update the CQRS query side data;

以上步骤必须严格按照上面的顺序一步步执行下来,否则无法确保逻辑正确。另外,以上流程目前只考虑单台机器,未考虑主备或集群的架构如何实现。

2013-02-18 11:04 "@tangxuehua"的内容
最终还是倾向于应该通过output event来做event sourcing ...

看你这么纠结,还是点破一下吧,从来就没有所谓input event或output event。event事件贯穿系统前后,系统任何部分包括领域都不会对event做任何改变。

那么领域发出的一些指挥架构的事件好像是output event,实际这只是 input event的一些分支,子事件而已。也就是说,所有的output event 都是input event的分支事件而已。

LMAX架构output event实际对于其处理者而言是一种input event,就象一个二叉树数据结构。


谢谢banq的回答。

LMAX架构明确指出,重建domain是用input event.
你的意思是:output event也是一种input event. 那command在LMAX架构中是否也是一种input event呢?

如果是,那是不是可以理解为LMAX是用command以及一部分output event来重建domain?
用一个简单的例子来说就是,ChangeNoteTitleCommand,NoteTitleChanged。一个是command, 一个是event。那按照你的意思,是不是可以理解为这两个都是LMAX的input event呢?还是只是NoteTitleChanged event才是其input event?

我就是这样,喜欢理解的非常明确,不喜欢只知道个概念。可能您觉得我过于刨根究底了,呵呵。
[该贴被tangxuehua于2013-02-18 11:42修改过]

2013-02-18 11:38 "@tangxuehua"的内容
output event也是一种input event. 那command在LMAX架构中是否也是一种input event呢? ...

晕倒,我的意思是完全没有input/output之说,如果一定要从input/output这个角度看,那么所有的event都是input event,可以参考一下GoF状态模式。

我可不想被你的“input/output之说”绕进去,你已经进去了还要拉别人进去,哈哈。

好吧,看来我们俩沟通存在一定问题,我发现总是我理解不了你或你理解不了我。看来我只能寻求它法了,新亏还有google group可以去讨论。

2013-02-19 11:02 "@tangxuehua"的内容
好吧,看来我们俩沟通存在一定问题 ...

非常遗憾,你的思考方向是向南,我的思考方向是向北,我认为你那个方向错了,你无法理解,还是继续在你的方向刨根问底下去,看待事物并不是一定在一个方向上能够找到答案,换个角度看,就非常清楚,横看成岭,侧成峰。


[该贴被banq于2013-02-19 14:15修改过]

你又理解错我的意思了。
这个和我们的方向无关。也和思维方式无关,不同的是现在我在关注一些问题,而你认为这些不是问题。也许将来有一天我也会通过我自己的方式找到了解决方案,也许有一天你也会突然去思考我以前曾经遇到过的问题。

对错无绝对,只是大家的关注点不同而已。我刨根究底问下去对你来说是方向错了,但从我的角度来说,我可能也会认为是你没思考到我的程度。所以我前面说你没真正理解我的意思一样。

我喜欢的是能正视问题,从逻辑严密的角度去分析我写的文字,而不是玩文字游戏。

2013-02-19 15:02 "@tangxuehua"的内容
我喜欢的是能正视问题,从逻辑严密的角度去分析我写的文字,而不是玩文字游戏 ...

非常赞赏你勤于思考问题,向你学习,我大概脑子不太愿意动了。生命在于折腾,如果你有一天将这个问题能考虑清楚,不要忘记分享,谢谢。

生命在于寻找细节,魔鬼在细节,细节决定最终的解决方案,呵呵。放心吧,我确实喜欢思考,你看我每次发问题基本都是一大段一大段的,而且尽量写出自己的思考过程,就能说明我不是一个随便简单理解大致原理的人,很多东西要真正掌握和用起来,不是简单的看看别人的一篇文章或几个简单的例子就行的,你说呢?

2013-02-19 16:26 "@tangxuehua"的内容
不是简单的看看别人的一篇文章或几个简单的例子就行的,你说呢? ...

我还能说什么呢?相当认可你的认真执着。

希望这篇文章对你解疑有帮助,你是搞C,源码比我精通,能否给大家解释一下?
用 F#和EventStore实现领域驱动设计

[该贴被banq于2013-02-19 17:01修改过]