2015年6月23日星期二

[.NET领域驱动设计实战系列]专题十:DDD扩展内容:全面剖析CQRS模式实现 - Learning hard

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
[.NET领域驱动设计实战系列]专题十:DDD扩展内容:全面剖析CQRS模式实现 - Learning hard  阅读原文»

一、引言

  前面介绍的所有专题都是基于经典的领域驱动实现的,然而,领域驱动除了经典的实现外,还可以基于CQRS模式来进行实现。本专题将全面剖析如何基于CQRS模式(Command Query Responsibility Segregation,命令查询职责分离)来实现领域驱动设计。

二、CQRS是什么?

  在介绍具体的实现之前,对于之前不了解CQRS的朋友来说,首先第一个问题应该是:什么是CQRS啊?你倒是详细介绍完CQRS后再介绍具体实现啊?既然大家会有这样的问题,所以本专题首先全面介绍下什么是CQRS。

  2.1 CQRS发展历程

  在介绍CQRS之前,我觉得有必要先了解一下CQS(即Command Query Separation,命令查询分离)模式。我们可以理解CQRS是在DDD的实践中基于CQS理论而出现的一种体系结构模式。CQS模式最早由软件大师Bertrand Meyer(Eiffel语言之父,面向对象开-闭原则OCP提出者)提出,他认为,对象的行为仅有两种:命令和查询,不存在第三种情况。根据CQS的思想,任何方法都可以拆分为命令和查询两部分。例如下面的方法:

private int _number = 0;
public int Add(int factor)
{
_number +
= factor;
return _number;
}

  在上面的方法中,执行了一个命令,即对变量_number加上一个因子factor,同时又执行了一个查询,即查询返回_number的值。根据CQS的思想,该方法可以拆成Command和Query两个方法:

private int _number = 0;
private void AddCommand(int factor)
{
_number
+= factor;
}

private int QueryValue()
{
return _number;
}

  命令和查询分离使得我们可以更好地把握对象的细节,更好地理解哪些操作会改变系统的状态。从而使的系统具有更好的扩展性,并获得更好的性能。

  CQRS根据CQS思想,并结合领域驱动设计思想,由Grey Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。CQRS将之前只需要定义一个对象拆分成两个对象,分离的原则按照对象中方法是执行命令还是执行查询来进行拆分的。

  2.2 CQRS结构

  由前面的介绍可知,采用CQRS模式实现的系统结构可以分为两个部分:命令部分和查询部分。其系统结构如下图所示:

  从上面系统结构图可以发现,采用CQRS实现的领域驱动设计与经典DDD有很大的不同。采用CQRS实现的DDD结构大体分为两部分,查询部分和命令部分,并且维护着两个数据库实例,一个专门用来进行查询,另一个用来响应命令操作。然后通过EventHandler操作将命令改变的状态同步到用来查询的数据库实例中。从这个描述中,我们可能会联想到数据库级别主从读写分离。然而数据读写分离是在数据库层面来实现读写分离的机制,而CQRS是在业务逻辑层面来实现读写分离机制。两者是站在两个不同的层面对读写分离进行实现的。

三、为什么需要引入CQRS模式

  前面我们已经详细介绍了CQRS模式,相信经过前面的介绍,大家对CQRS模式一定有一些了解了,但为什么要引入CQRS模式呢?

  在传统的实现中,对DB执行增、删、改、查所有操作都会放在对应的仓储中,并且这些操作都公用一份领域实体对象。对于一些简单的系统,使用传统的设计方式并没有什么不妥,但在一些大型复杂的系统中,传统的实现方式也会存在一些问题:

  • 使用同一个领域实体来进行数据读写可能会遇到资源竞争的情况。所以经常要处理锁的问题,在写入数据的时候,需要加锁,读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并会影响系统的吞吐量。
  • 在大数据量同时进行读写的情况下,可能出现性能的瓶颈。
  • 使用同一个领域实体来进行数据库读写可能会太粗糙。在大多是情况下,比如编辑操作,可能只需要更新个别字段,这时却需要将整个对象都穿进去。还有在查询的时候,表现层可能只需要个别字段,但需要查询和返回整个领域实体,再把领域实体对象转换从对应的DTO对象。
  • 读写操作都耦合在一起,不利于对问题的跟踪和分析,如果读写操作分离的话,如果是由于状态改变的问题就只需要去分析写操作相关的逻辑就可以了,如果是关于数据的不正确,则只需要关心查询操作的相关逻辑即可。

  针对上面的这些问题,采用CQRS模式的系统都可以解决。由于CQRS模式中将查询和命令进行分析,所以使得两者分工明确,各自负责不同的部分,并且在业务上将命令和查询分离能够提高系统的性能和可扩展性。既然CQRS这么好,那是不是所有系统都应该基于CQRS模式去实现呢?显然不是的,CQRS也有其使用场景:

  1. 系统的业务逻辑比较复杂的情况下。因为本来业务逻辑就比较复杂了,如果再把命令操作和查询操作绑定同一个业务实体的话,这样会导致后期的需求变更难于进行扩展下去。
  2. 需要对系统中查询性能和写入性能分开进行优化的情况下,尤其读/写比例非常高的情况下。例如,在很多系统中读操作的请求数远大于写操作,此时,就可以考虑将写操作抽离出来进行单独扩展。
  3. 系统在将来随着时间不断变化的情况下。

  然而,CQRS也有其不适用的场景:

  • 业务逻辑比较简单的情况下,此时采用CQRS反而会把系统搞的复杂。
  • 系统用户访问量都比较小的情况下,并且需求以后不怎么会变更的情况下。针对这样的系统,完全可以用传统的实现方式快速将系统实现出来,没必要引入CQRS来增加系统的复杂度。

四、事件溯源

  在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回,这个方面的操作相对比较简单。而命令方面,是通过发送具体Command,接着由CommandBus来分发到具体的CommandHandle来进行处理,CommandHandle在进行处理时,并没有直接将对象的状态保存到外部持久化结构中,而仅仅是从领域对象中获得产生的一系列领域事件,并将这些事件保存到Event Store中,同时将事件发布到事件总线Event Bus进行下一步处理;接着Event Bus同样进行协调,将具体的事件交给具体的Event Handle进行处理,最后Event Handler再把对象的状态保存到对应Query数据库中。

  上面过程正是CQRS系统中的调用顺序。从中可以发现,采用CQRS实现的系统存在两个数据库实例,一个是Event Store,该数据库实例用来保存领域对象中发生的一系列的领域事件,简单来说就是保存领域事件的数据库。另一个是Query Database,该数据库就是存储具体的领域对象数据的,查询操作可以直接对该数据库进行查询。由于,我们在Event Store中记录领域对象发生的所有事件,这样我们就可以通过查询该数据库实例来获得领域对象之前的所有状态了。所谓Event Sourcing,就是指的的是:通过事件追溯对象的起源,它允许通过记录下来的事件,将领域模型恢复到之前的任意一个时间点。

  通过Event来记录领域对象所发生的所有状态,这样利用系统的跟踪并能够方便地回滚到某一历史状态。经过上面的描述,感觉事件溯源一般用于系统的维护。例如,我们可以设计一个同步服务,该服务程序从Event Store数据库查询出领域对象的历史数据,从而打印生成一个历史报表,如历史价格报表等。但正是的CQRS系统中如何使用Event Sourcing的呢?

  在前面介绍CQRS系统的调用顺序中,我们讲到,由Event Handler将对象的状态保存到对应的Query数据库中,这里有一个问题,对象的状态怎么获得呢?对象状态的获得正是由Event sourcing机制来获得,因为用户发送的仅仅是Command,Command中并不包含对象的状态数据,所以此时需要通过Event Sourcing机制来查询Event Store来还原对象的状态,还原根据就是对应的Id,该Id是通过命令传入的。Event Sourcing的调用需要放在CommandHandle中,因为CommandHandle需要先获得领域对象,这样才能把领域对象与命令对象来进行对比,从而获得领域对象中产生的一系列领域事件。

五、快照

  然而,当随着时间的推移,领域事件变得越来越多时,通过Event Sourcing机制来还原对象状态的过程会非常耗时,因为每一次都需要从最早发生的事件开始。那有没有好的一个方式来解决这个问题呢?答案是肯定的,即在Event Sourcing中引入快照(Snapshots)实现。实现原理就是——没产生N个领域事件,则对对象做一次快照。这样,领域对象溯源的时候,可以先从快照中获得最近一次的快照,然后再逐个应用快照之后所有产生的领域事件,而不需要每次溯源都从最开始的事件开始对对象重建,这样就大大加快了对象重建的过程。

六、CQRS模式实现和剖析

  前面介绍了那么多CQRS的内容,下面就具体通过一个例子来演示下CQRS系统的实现。

  命令部分的实现

堆表上的转发记录 - woodytu  阅读原文»

今天这篇文章我想谈下堆表上特有的性能问题:转发记录(Forwarding Records)。首先我们要澄清下什么是堆表:堆表就是没有聚集索引定义的表。它对插入新记录非常快,但当你读取数据时非常慢。读取数据会在你的存储子系统上引入随机存取(random I/O) 有时候当你碰到转发记录,它会进一步降低你的读取性能。

为什么会有转发记录?

当堆表里的记录需要移动到不同的物理位置时,SQL Server使用转发记录。假设你有一个变长列的表,首先你在堆表里插入一些记录,这个时候你没有在变长列里存储任何数据:

1 -- Create a table to demonstrate forwarding records
2 CREATE TABLE HeapTable
3 (
4 Col1 INT IDENTITY(1, 1),
5 Col2 CHAR(2000),
6 Col3 VARCHAR(1000)
7 )
8 GO
9
10 -- Insert 4 records - those will fit into one page
11 INSERT INTO HeapTable VALUES
12 (
13 REPLICATE('1', 2000),
14 ''
15 ),
16 (
17 REPLICATE('2', 2000),
18 ''
19 ),
20 (
21 REPLICATE('3', 2000),
22 ''
23 ),
24 (
25 REPLICATE('4', 2000),
26 ''
27 )
28 GO

当你在变长列执行UPDATE语句时,想象下会发生什么?在那个情况下SQL Server可能需要扩展这个记录,因为记录大小更长了,其他记录必须从同个数据页移走。

1 -- Let's update the table and expand each row of the table
2 UPDATE HeapTable
3 SET Col3 = REPLICATE('5', 1000)
4 GO

在那个情况下,SQL Server在原始位置留下称为转发记录,它指向记录最终存储的新位置。

SQL Server需要使用这个方法来避免更新同个表上所有非聚集索引。你可能知道,当你在堆表上创建非聚集索引时,在叶子层,非聚集索引指向记录数据存储的物理位置。没有抓发记录的话,所有这些指针都要改变,这会大幅度降低你的性能。

如何修正转发记录?

为了找出表里是否包含转发记录,你可以使用DMF sys.dm_db_index_physical_stats。当你在堆表上调用这个函数并传入DETAILED模式,SQL Server通过forwarded_record_count列告诉你表上的转发记录数。

1 -- Check the forwarding record count through sys.dm_db_index_physical_stats
2 SELECT
3 index_type_desc,
4 page_count,
5 avg_page_space_used_in_percent,
6 avg_record_size_in_bytes,
7 forwarded_record_count
8 FROM sys.dm_db_index_physical_stats
9 (
10 DB_ID('ALLOCATIONDB'),
11 OBJECT_ID('HeapTable'),
12 NULL,
13 NULL,
14 'DETAILED'
15 )
16 GO

可以看到,表上有2条转发记录,在4条记录之外。为了去掉这些转发记录,你可以重建表。

1 -- Rebuild the heap table to get rid of the Forwarding Records
2 ALTER TABLE HeapTa

阅读更多内容

没有评论:

发表评论