[关闭]
@Rays 2017-01-21T09:35:03.000000Z 字数 6438 阅读 2141

使用聚合、事件溯源和CQRS开发事务型微服务(第一部分)


摘要: 开发基于微服务的业务应用时所面对的一个挑战,就是事务、领域模型和查询对功能分解的抵制。本文作者给出了一种通过领域驱动设计(DDD)聚合对领域模型进行分解的方法。在该方法中,每个服务的业务逻辑构成一个领域模型,每个领域模型由一个或更多的DDD聚合组成。在每个服务中,一个事务建立或更新一个聚合,并使用事件去维持聚合间的一致性。

作者:Chris Richardson

正文:

关键要点:

  • 微服务架构将应用功能分解为多个服务,其中每个服务都对应于一种业务能力。
  • 开发基于微服务的业务应用时,一个主要挑战是功能分解在事务、领域模型和查询中受到了抵制。
  • 领域模型可分解为多个领域驱动设计(Domain Driven Design,DDD)聚合。
  • 每个服务的业务逻辑都是一个领域模型,该领域模型由一个或更多的DDD聚合所组成。
  • 在服务中,每个事务只对单一的聚合进行创建或更新。
  • 事件用于维持聚合间(服务间)的一致性。

微服务架构正变得越来越流行。作为一种实现模块化的方法,微服务架构将应用功能分解为一组服务。团队可使用微服务架构去开发大型复杂的应用,这样可以更块地交付更好的软件。微服务架构还使得在开发中更易于采用新的技术,因为团队可使用最新最适合的技术栈去对每个服务进行实现。此外,通过使每个服务都可以部署在最优的硬件上,微服务架构可改进应用的可扩展性。

但是微服务并非是一剂良方。尤其是在领域模型、事务和查询中,功能分解受到了出乎意料地抵制。由于这个问题的存在,使用微服务架构开发事务性业务应用是一件具有挑战性的事情。在本文中,我将给出了一种微服务开发的方法,该方法使用领域驱动设计、事件溯源和命令查询职责分离(Command Query Responsibility Segregation,CQRS)技术解决了上述的问题。让我们首先看一下开发人员在编写微服务中所面对的挑战。

微服务开发中的挑战

在大型复杂应用的开发中,模块化是至关重要的。大多数现代应用的规模决定了它不可能由单个开发人员单枪匹马地开发实现,这些应用的复杂度也不可能被单个开发人员所理解。这样应用必须要被分解为模块,而模块是可被已形成团队的开发人员理解并开发实现的。在整体应用程序中,可使用诸如Java包这样的编程语言结构定义模块。但这种方法常常在实践中并不好用。长生命周期的整体应用程序通常会退化为一个“大泥球”(译者注:指一个微服务中的问题将会扩展到整个应用中)。

微服务架构使用服务作为模块化单元。每个服务对应于一种业务能力,即企业为创造价值所执行的操作。这里以一个基于微服务的网店为例。该网店由各种服务所组成,其中包括订单服务(Order Service)、客户服务(Customer Service)和商品目录服务(Catalog Service)。

每个服务都具有难以违反的防渗透边界。正是由于该原因,应用的模块化的保持是经得起时间考验的。微服务架构的其它优点还包括可独立地部署和扩展服务的能力。

遗憾的是,将应用分解为服务实现起来并非听上去那么容易。应用中有很多不同的方面是难以被分解的,这包括了领域模型、事务和查询。下面让我们看一下为什么会这样。

问题一:分解领域模型

领域模型模式是一种实现复杂业务逻辑的好方法。网店的领域模型可包括Order(订单)类、OrderLineItem(订单条目)类、Customer(客户)类和Product(商品)类等。在微服务架构中,Order类和OrderLineItem类是订单服务的一部分,Customer类是客户服务的一部分,而Product类属于商品目录服务。

然而,分解领域模型的挑战性在于各个类之间的相互引用。例如,Order类会引用其Customer类,OrderLineItem类引用Product类。该如何去处理这些跨越了服务边界的引用关系呢?在本文后面的内容中,读者将读到如何使用源于领域驱动设计的聚合概念去解决这个问题。

微服务和数据库

微服务的一个特有特性是,服务所拥有的数据只能通过该服务提供的API访问。回到网店的例子上,订单服务的数据库中有一个ORDERS表,客户服务的数据库中有一个CUSTOMERS表。这种封装表明服务是松耦合的。在开发时,订单服务和客户服务的开发人员无需与其它服务的开发者进行协调,就可以去改变这两个服务的服务模式。在运行时服务之间是相互独立。例如,一个服务将永远不会因为等待其它服务所拥有的数据库锁而被阻塞。遗憾的是,该数据库按功能的分解导致了数据一致性难于维持,还导致一些类型的查询难于实现。

问题二:实现跨越多个服务的事务

为强化应用中的业务规则(也称为不变式),传统的整体应用程序可依赖于ACID事务性。在网店的例子中,如果在客户建立新订单前,我们必须要去检查该客户的信用额度,这就必须确保多个并发的潜在订单尝试不会产生超过客户信用额度的问题。如果Order类和Customer类使用同一个数据库,我们可以轻易地将其实现为如下的ACID事务(具有适当的事务隔离度):

  1. BEGIN TRANSACTION
  2. SELECT ORDER_TOTAL
  3. FROM ORDERS WHERE CUSTOMER_ID = ?
  4. SELECT CREDIT_LIMIT
  5. FROM CUSTOMERS WHERE CUSTOMER_ID = ?
  6. INSERT INTO ORDERS
  7. COMMIT TRANSACTION

可惜在基于微服务的应用中,我们并不能使用上面这种直接方法去维持数据一致性。因为数据库表ORDERS和CUSTOMERS被不同的服务所有,并只能通过API进行访问。这两个表甚至可能在不同的数据库中。

对此,传统的解决方案是两阶段提交 (也称为分布式交易),但对于当代应用而言,两阶段提交并非是一种可行的技术。依据CAP定理,应用需要在可用性和一致性两者间做出抉择,通常首选可用性。进一步讲,一些当代技术甚至都不支持ACID事务,例如大多数NoSQL数据库,更不用说两阶段提交了。考虑到维持数据一致性的重要性,我们需要其它的解决方案。本文后面将给出这样的解决方案,就是使用基于被称为“事件溯源”的事件驱动架构。

问题三:查询与报表

维持数据一致性并非是唯一的挑战,另一个问题是查询与报表。在传统的整体应用程序中,编写使用连接运算的查询是十分常见的。例如使用如下查询,读者能很轻易地查到近期网店的客户及由他们所做的一定规模的订单:

  1. SELECT *
  2. FROM CUSTOMER c, ORDER o
  3. WHERE
  4. c.id = o.ID
  5. AND o.ORDER_TOTAL > 100000
  6. AND o.STATE = 'SHIPPED'
  7. AND c.CREATION_DATE > ?

但这类查询并不能用于基于微服务的网店中。正如前面所提及的,数据库表ORDERS和CUSTOMERS被不同的服务所拥有,并只能通过API访问。甚至可能有一些服务不会去采用SQL数据库。后文中读者将看到在一些其它服务中,使用了称为一种“时间溯源”的方法,使得查询更加具有挑战性。稍后,让我们看一下在为实现微服务而进行基于领域模型的业务逻辑开发时,领域驱动设计是如何成为一种不可或缺的工具的。

DDD聚合是微服务的构建模块

正如读者所看到的,为成功地使用微服务架构开发业务应用,有一些问题必须要得到解决。在Eric Evans所著的经典书籍《领域驱动设计》中,可为其中的一些问题找到解决方法。该书出版于2003年,描述了复杂软件的设计方法,对于微服务开发是十分有用的。尤其是,该书指出领域驱动设计可用于创建跨服务分区的模块化领域模型。

什么是聚合?

Evans为领域驱动设计中的领域模型定义了一系列的构建模块。一些构建模块已成为日常开发语言的组成部分,其中包括:实体,即具有持久标识对象;值对象,即虽然没有标识但是由自身属性所定义的对象;服务,包含了不属于任何实体或值对象服务的业务逻辑;仓库,表示一系列的持久实体。除纯粹领域驱动设计主义者之外,聚合作为其中的一种构件却几乎被所有的开发人员所忽视。但是对于微服务开发,聚合已被证实是十分关键的。

聚合是可被视为同一整体的一组领域对象。聚合由根实体以及一个或多个其它关联视图和值对象所组成。例如,网店的领域模型包括了Order和Customer等领域对象。Order聚合的组成包括Order实体(即根实体)和一个或多个的OrderLineItem值对象,还有DeliveryAddress和PaymentInformation等一些其它的值对象。Customer聚合的组成包括Customer根实体以及DeliveryInfo和PaymentInformation等一些其它的值对象。

领域模型可用聚合分解成组块(chunk),每个组块自身是易于被理解的。这样就澄清了加载和删除等操作的范畴问题。聚合通常全部从数据库加载。删除一个聚合也就删除了其所有的对象。但是使用聚合的优点远远超出了领域模型的模块化。这是因为聚合必须要遵守确定的规则。

聚合间的引用必须使用主键

第一条规则就是聚合间通过标识(例如主键)来引用,而非通过对象引用。例如Order使用customerId引用其Customer,而非引用Customer对象。同样,OrderLineItem使用productId引用Product。

该方法完全不同于传统的对象建模。在对象建模中,领域模型中的外键被认为是“设计异味”(译者注:设计异味,Design Smell,指设计中违反了基本的设计原则并对设计的质量产生了负面影响的特定结构)。使用标识而非对象引用,这意味着聚合是松耦合的。这样开发人员易于将不同的聚合置于不同的服务中。事实上,一种服务的业务逻辑构成一个领域模型,每个领域模型由一组聚合组成。例如,订单服务包含了Order聚合,客户服务包含了Customer聚合。

一个事务创建或更新一个聚合

聚合中必须要遵循的第二条规则是,一个事务只能去创建或更新一个聚合。我在很多年前第一次读到该规则时,感觉真是让人无法理解!在那时,我正在开发传统的基于RDBMS的整体应用,事务可以去更新任意的数据。但时至今日,这条规则对于微服务架构而言无疑是完美的。它确保了一个事务包含于一个服务之中。该规则也适合大多数NoSQL数据库的受限事务。

在领域模型的开发中,确定每个聚合的规模是开发人员必须要做出的决策。从一个方面讲,理想的聚合应该是越小越好。这是因为聚合通过分离问题改进了模块化,也因为聚合通常是完全加载的,这样细粒度的聚合就是更加高效的。此外,考虑到聚合的更新是顺序发生的,使用细粒度聚合将增加应用可处理的同步请求的个数,进而改进了可扩展性。再有,细粒度聚合降低了两个用户视图更新同一聚合的可能性,改进了用户体验。从另一方面讲,因为聚合属于事务的范畴,为保证特定更新操作的原子性,开发人员可能需要定义更大规模的聚合。

在本文的前面,我以网店的领域模型为例介绍了如何将Order聚合和Customer聚合定义为不同的聚合。另一种设计方案是将所有的Order聚合作为Customer聚合的组成部分。由此得到的大型Customer聚合的优点在于,应用可原子性地强制实施信用检查。但这种设计方案的缺点在于将订单和客户管理功能整合到了同一服务中。考虑到更新同一客户不同订单的事务将被序列化,该设计方案降低了应用的可扩展性。同样的原理,在两个用户试图去编辑同一客户的不同订单时可能会产生冲突。此外,加载Customer聚合的代价会随订单数量的增长而日益增高。由于以上的原因,我们应使聚合尽可能地细粒度。

尽管一个事务只能创建或更新一个聚合,应用仍需去维持聚合间的一致性。例如订单服务必须要验证一个新的Order聚合没有超出Customer聚合的信用额度。维持一致性有两种可选的做法。一种做法是在同一事务中对多个聚合进行核查、创建或更新。这种方法仅适用于所有聚合归属于同一服务并在同一RDBMS中持久化的情况。另一种更正确的做法,是使用最终一致性且事件驱动的方法去维持聚合间的一致性。

使用事件维持数据一致性

在现代应用中,在事务上有各种各样的限制,这使得在服务间维持一致性是十分具有挑战性的。每个服务具有其自身的数据,这使得两阶段提交并非是一种可用的方法。此外,不少应用使用了NoSQL数据,而NoSQL数据库并不支持本地ACID事务,更不用说分布事务了。因此,现代应用必须使用一种事件驱动的最终一致性事务模型。

什么是事件?

依据韦氏词典的定义,“事件”就是所发生的事情,在韦氏词典中对事件定义的原文如下图:

在本文中,我们将“领域事件”定义为在聚合上所发生的事情。事件通常表示了状态的改变。例如,对于网店应用中的Order聚合而言,它的状态改变事件包括创建订单、取消订单、发送订单等。事件表示了违反业务规则的尝试,例如违反客户的信用额度。

使用事件驱动架构

服务使用事件去维持聚合间的一致性,其做法是:在任何值得注意的情况发生时,聚合就发布一个事件,这些情况可能是聚合的状态改变,或是存在违反业务规则的可能性时。其它聚合订阅事件,并通过更新自身状态对事件进行响应。

网店在创建一个订单时,采用如下步骤去验证一个客户的信用额度:

  1. 以“NEW”状态创建的Order聚合,并发布一个OrderCreated事件;
  2. Customer聚合消费OrderCreated事件,为订单保存信用值,并发布CreditReserved事件;
  3. Order聚合消费CreditReserved事件,并将自身状态更改为“APPROVED”。

在因资金不足而导致信用额度检查失败时,Customer聚合就会发布一个CreditLimitExceeded事件。该事件并不对应于一个状态改变,而是表示了违反业务规则的失败尝试。Order聚合消费该事件,并将自身状态更改为“CANCELLED”。

以微服务架构作为事件驱动聚合的网络

在这种架构中,每个服务的业务逻辑中包含了一个或多个聚合。每个事务通过一个服务创建或更新一个聚合来执行。服务使用事件维持聚合间的数据一致性。

该方法的一个特有优点是将聚合作为松耦合的构件。聚合被部署到一个整体或是一组服务中。在一个项目的开始阶段,开发人员可以使用整体架构。此后,随着应用及开发团队规模的增加,开发人员可轻易地迁移到微服务架构上。

总结

微服务架构将应用功能分解为服务,每个服务对应于一种业务能力。事务、领域模型和查询对分解的抵制,是开发基于微服务的业务应用时的一个挑战。可以通过使用DDD聚合的理念对领域模型进行分解。每个服务的业务逻辑构成一个领域模型,每个领域模型由一个或更多的DDD聚合组成。

在每个服务中,一个事务建立或更新一个聚合。因为对当代应用而言,两阶段提交并非是一种可行的技术,我们使用事件维持聚合间(和服务间)的一致性。在本文随后的第二部分中,我们将描述如何使用“事件溯源”实现一个可靠的事件驱动架构。我们还会展示如何在微服务架构中使用命令查询职责分离实现查询。

关于本文作者:

Chris Richardson是一位程序开发人员和架构师。他还是一名Java冠军程序员(Java Champion),同时也是《用轻量级框架开发企业应用》一书的作者。这本书的内容是关于如何使用Spring和Hibernate等架构去构建企业Java应用的。Chris也是CloudFoundry.com的创始人。他还担任了一些企业的顾问,指导企业如何去改进应用的开发和部署过程。他目前正致力于发展他的第三个初创企业。读者可以通过Twitter账号@crichardsonEventuate网站联系到Chris。

查看英文原文:Developing Transactional Microservices Using Aggregates, Event Sourcing and CQRS - Part 1

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注