欢迎光临汤雪华的博客

一个人一辈子能坚持做好一件事情就够了!坚持是一种刻意的练习,不断寻找缺点突破缺点的过程,而不是重复做某件事情。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

最近又学习了一下LMAX架构,让我对该架构以及event sourcing模式又有了很多新的认识和疑问。

注:如果不知道什么是lmax架构和event sourcing模式的看官可以自己先去查查资料:

  • LMAX可以看看martin写的一篇文章:http://martinfowler.com/articles/lmax.html
  • Event Sourcing的资料比较多,随便google一下即可。
  • 当然,我的博客里也有大量关于这两个方面的笔记,有兴趣的可以看看。

下面是我的一些最新的想法。

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的方式来控制整个业务流程;首先也是由aggregate来处理input event,然后产生的output event会由saga响应,然后saga会根据流程逻辑决定接下来要做什么,即产生哪个input 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一定已经被持久化了呢?

目前就这些了,以后有更多的思考内容再补充上来。

-------------------------------------------------------------------------------------------------------------------------------

后来又做了一些思考。

想来想去,最终还是倾向于应该通过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;

以上步骤必须严格按照上面的顺序一步步执行下来,否则无法确保逻辑正确。
另外,以上流程目前只考虑单台机器,未考虑主备或集群的架构如何实现;之所以用英文写是因为我还要拿去和老外讨论,呵呵。不过这几句英文应该比较简单吧。