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

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

  第一条规则就是聚合间通过标识(例如主键)来引用,而非通过对象引用。例如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聚合而言,它的状态改变事件包括创建订单、取消订单、发送订单等。事件表示了违反业务规则的尝试,例如违反客户的信用额度。

  使用事件驱动架构

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