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

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

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

  BEGIN TRANSACTION

  …

  SELECT ORDER_TOTAL

  FROM ORDERS WHERE CUSTOMER_ID = ?

  …

  SELECT CREDIT_LIMIT

  FROM CUSTOMERS WHERE CUSTOMER_ID = ?

  …

  INSERT INTO ORDERS …

  …

  COMMIT TRANSACTION

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

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

  问题三:查询与报表

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

  SELECT *

  FROM CUSTOMER c, ORDER o

  WHERE

  c.id = o.ID

  AND o.ORDER_TOTAL > 100000

  AND o.STATE = 'SHIPPED'

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