欢迎光临汤雪华的博客

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

ENode 1.0 - 事件驱动架构(EDA)思想的在框架中如何体现

Posted on 2013-06-19 00:00  netfocus  阅读(6359)  评论(17编辑  收藏

开源地址:https://github.com/tangxuehua/enode

上一篇文章,我给大家分享了我的一个基于DDD以及EDA架构的框架enode,但是只是介绍了一个大概。接下来我准备用很多一篇篇详细但不冗长的文章介绍每个点。尽量争取一次不介绍太多内容,但希望每次介绍完后都能让大家知道这个小点的设计思想,以及为了解决的问题。

好了,这篇文章,我主要想介绍的是EDA思想在enode框架中如何体现?

经典DDD的基于领域服务的实现方式

一般的应用程序,如果一个用户动作会涉及多个聚合根的修改,我们通常会在应用层服务中创建一个unit of work,然后,我们可能会设计一个领域服务类,在该领域服务类里,修改多个聚合根,然后应用层服务将整个unit of work中的修改一次性以事务的方式提交到数据库。这种方式就是以事务的方式来实现涉及多个聚合根修改的强一致性。以银行转账这个经典的场景作为分析案例:

    public interface IBankAccountService
    {
        void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount);
    }
    public class BankAccountService : IBankAccountService
    {
        private IContextManager _contextManager;
        private TransferMoneyService _transferMoneyService;

        public BankAccountService(IContextManager contextManager, TransferMoneyService transferMoneyService)
        {
            _contextManager = contextManager;
            _transferMoneyService = transferMoneyService;
        }

        public void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount)
        {
            using (var context = _contextManager.GetContext())
            {
                var sourceAccount = context.Load<BankAccount>(sourceBankAccountId);
                var targetAccount = context.Load<BankAccount>(targetBankAccountId);
                _transferMoneyService.TransferMoney(sourceAccount, targetAccount, amount);
                context.SaveChanges();
            }
        }
    }

一次银行转账,最核心的动作就是源账号转出钱,目标账号转入钱;当然实际的银行转账肯定不是这么简单,也肯定不是这么实现。我拿这个作为例子只是为了通过这个大家都熟知的简单例子来分析如果一个用户场景涉及不止一个聚合根的修改的时候,如果基于经典的DDD的方式,我们是如何实现的。如上面的代码所示,我们可能会设计一个应用层服务,如上面的IBankAccountService,该应用层服务里有一个TransferMoney的方法,表示用于实现银行转账的功能;然后该应用层服务会进一步调用一个领域层的转账领域服务,就是上面代码中的TransferMoneyService,按照Eric Evans所说,领域服务应该是一个以动词命名的服务,一个领域服务可以明确对应到领域中的一个有业务含义的领域动作,此例就是“转账”,所以我设计了一个TransferMoneyService的以动词来命名的领域服务,该服务的TransferMoney方法实现了银行转账的核心业务逻辑。

上面这个例子中,按照经典DDD,我们应该在应用层实现流程控制逻辑以及事务等东西;所以大家可以看到,以上代码中,我们是先获取一个unit of work,即上面代码中的context,最后调用context.SaveChanges方法,该方法的职责就是将当前上下文的所有修改以事务的方式提交到数据库。好了,上面这个例子我们分析了经典DDD关于如何实现一个会涉及多个聚合根新建或修改的用户场景;

enode的事件驱动的实现方式

我一直说enode是一个基于事件驱动架构(EDA,Event-Driven Architecture)的框架。且深蓝医生在前面的回复中也对什么是事件驱动的架构有疑惑。所以我想说一下我对事件驱动架构的理解。

EDA,顾名思义,我觉得就是事件驱动的,那事件到底驱动了什么呢?我觉得就是事件驱动状态的修改。如何理解呢?就是说,假如你要修改一个对象的状态,那就不是直接调用该对象的某个方法来修改它或者直接通过修改某个对象的属性来达到修改该对象状态的目的;取而代之的是,我们需要先触发一个事件,然后该对象会响应该事件,然后在响应函数中修改对象自己的状态。当然,更广义和权威的事件驱动架构的定义和解释,我觉得很容易找啊,比如直接去百度上搜一下或直接到wikipedia上搜一下,也很容易就能找到标准的解释。比如这里就是我找到的解释。其实,更大范围的解释,就是一种publish-subscriber模式,就是有一个事件生产者产生事件,然后有一个类似event publisher的东西会把这个事件广播出去,然后所有的事件消费者就能消费该事件了。通过这样的pub-sub,我们的应用程序的各个组件之间可以做到很彻底的解耦,并且可以做到更灵活的扩展性。这两点的好处应该是很容易体会到的。比如更彻底的解耦是,比如本来一个对象要和另一个对象交互,那它可能要引用该对象,然后调用该对象的某个方法,从而实现对象之间的交互。这种实现方式会让两个对象绑定在一起,比如a对象调用b对象的方法,那意味着a需要依赖b对象;而通过事件驱动的方式,a对象只要publish一个事件,然后b对象响应该事件即可,这样a对象就不知道b对象的存在了,也就是a对象不在依赖b对象;扩展性,就是本来一个事件,可能只有1个事件响应者,但是后面可能由于功能扩展等原因,我们需要增加一个事件响应者,这样就能方便的做到在不改变原来任何代码的基础之上,增加新功能了;其他的好处就不多分析了,有兴趣的可以再去看看资料吧。

上面这一段,我简单介绍了我所理解的EDA,以及它的基本的好处。下面我们看看,在enode中,我们是如何利用EDA这种原理的。为了简化,我先用一个简单的例子说明一下,就用我源代码中的NoteSample吧,反正也能一样说明事件驱动的影子在哪里。看以下的代码:

    [Serializable]
    public class Note : AggregateRoot<Guid>,
        IEventHandler<NoteCreated>,     //订阅事件
        IEventHandler<NoteTitleChanged>
    {
        public string Title { get; private set; }
        public DateTime CreatedTime { get; private set; }
        public DateTime UpdatedTime { get; private set; }

        public Note() : base() { }
        public Note(Guid id, string title) : base(id)
        {
            var currentTime = DateTime.Now;
            //触发事件
            RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime));
        }

        public void ChangeTitle(string title)
        {
            //触发事件
            RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now));
        }

        //事件响应函数
        void IEventHandler<NoteCreated>.Handle(NoteCreated evnt)
        {
            //在响应函数中修改自己的状态,这里可以体现出EDA的影子,就是事件驱动状态的修改
            Title = evnt.Title;
            CreatedTime = evnt.CreatedTime;
            UpdatedTime = evnt.UpdatedTime;
        }
        //事件响应函数
        void IEventHandler<NoteTitleChanged>.Handle(NoteTitleChanged evnt)
        {
            //同上解释
            Title = evnt.Title;
            UpdatedTime = evnt.UpdatedTime;
        }
    }

 

上面的例子中,Note是一个聚合根,它会响应两个事件:NoteCreated, NoteTitleChanged。要实现事件响应,我们可以通过实现框架提供的IEventHandler<T>接口,就能告诉框架,我要订阅什么事件了。

上面代码中,应该比较详细的注释了每段代码的含义了,应该都能看懂吧。上面这个例子说明了,聚合跟自己的状态不是在public方法中直接改的,而是基于事件驱动的方式来修改的,所以,大家可以看到,聚合根状态的修改是在一个内部响应函数中修改的。下面我们再来看一下外部其他对象,如何响应该事件:

    //这是一个事件订阅者,它也响应了Note的两个事件
    public class NoteEventHandler :
        IEventHandler<NoteCreated>,
        IEventHandler<NoteTitleChanged>
    {
        public void Handle(NoteCreated evnt)
        {
            //这里为了简单,所以只是输出了一串文字,实际我们可以在这里做任何你想做的事情;
            Console.WriteLine(string.Format("Note created, title:{0}", evnt.Title));
        }
        public void Handle(NoteTitleChanged evnt)
        {
            Console.WriteLine(string.Format("Note title changed, title:{0}", evnt.Title));
        }
    }

通过上面两个简单的例子,不知道有没有解释清楚,在enode框架中,如何体现EDA?

总结:

我之所以比较喜欢事件驱动这种思想是基于以下理由:

  1. 就是上面我说的解耦+可扩展;
  2. 事件可以并行执行;就是说,一个系统中,同时可以有很多事件在并行的产生、传递、响应;这样说,大家可能还理解不了这一点的价值。我说一下并发的概念。通常我们所说的一个网站的并发,比如有5000,是指一个网站在1秒内的所有并发请求数,这么多并发请求数是针对系统中所有的聚合根的;也就是如果平摊到每个聚合根,那并发修改数一般就很低了,比如每秒只有10个并发,甚至只有1个或两个。这点每个系统有所不同,比如淘宝的商品秒杀活动,那当秒杀开始的时刻,对同一个商品的下单的并发数很高,因为每个商品的每个订单都意味着要减库存,所以这个减库存的并发操作一定很高,实现起来肯定很困难了,不通过可靠的分布式缓存以及乐观锁机制,估计很难实现;而比如新浪微博上,我们每个人发微博,虽然整个新浪微博网站的整体并发数很高,因为肯定每秒有非常多的人在写微博,但是我们同时也知道,大家写的微博都是独立的,没有共享资源,每发表一条微博实际上就是创建一条数据库记录而已。所以可以理解为,单个对象无并发;而一般的企业应用或一般的互联网应用,针对同一资源(同一个聚合根)的并发修改,一般都不高;所以基于这样的分析和理解,我们知道了,理论上,事件什么时候可以并行产生和执行,什么时候必须排队。就是:如果两个事件不是同一个聚合根产生的,那就可以并行处理,事件也可以并行持久化;如果是单个聚合根产生的,那必须按照顺序被持久化;所以,根据这样的理解,我们知道了,一个应用程序,除了单个聚合根上的修改只能串行进行外,其他情况理论上都可以并行执行;这段话说了这么多关于并发数以及事件并行方面的东西,那究竟知道这些有什么用呢?很简单,只要和传统的事务模式对比下就知道了,传统的事务模式,如果要修改多个聚合根,那事务在执行的那一段时间,所有涉及到的聚合根都不能被其他事务所修改;只有等到当前事务执行完成后,其他事务才能执行;而通过事件的方式,由于我们没有事务的概念,我们唯一要确保的只是一个聚合根上产生的事件必须被一个个按顺序持久化,这点我们很简单,比如我们只要建一个联合主键:聚合根ID+事件版本号,然后做乐观并发控制即可;所以,事件持久化时,排他的粒度比事务要小,这样的好处是无阻塞;那么换来的好处就是网站整体的可用性高;但是带来的坏处是,可能有可能会出现乐观并发冲突,但这点我们可以通过框架的自动重试功能解决掉;而且,我们也刚分析过,同一个聚合根的并发修改一般是很低的;所以通过事件的方式来达到这种细粒度的对聚合根的修改是非常有意义的。
  3. 配合Event Sourcing模式,可以让EDA发挥更大的价值,更准确的说,我们可以让事件发挥更大的价值;就是:我们不仅可以让事件作为消息,在系统各个对象或组件甚至是各个系统之间传递,还可以用事件来还原整个系统的状态。这点我会在后面详细介绍enode框架中如何使用event sourcing这种模式;