克里斯·理查森(ChrisRichardson)。
单块应用程序通常有一个关系数据库。使用关系数据库的一个主要好处是应用程序可以使用酸性交易提供了一些重要的保障:
因此,应用程序可以简单地启动事务、更改(插入、更新和删除)多行,并提交事务。
使用关系数据库的另一个好处是它提供了SQL,SQL是一种丰富、声明性和标准化的查询语言。您可以轻松地编写一个将多个表中的数据组合在一起的查询。然后,RDBMS查询计划器确定执行查询的最优方式。您不必担心低级别的细节,比如如何访问数据库。而且,由于应用程序的所有数据都位于一个数据库中,所以很容易查询。
不幸的是,当我们转向微服务体系结构时,数据访问变得更加复杂。这是因为每个微服务拥有的数据是私人的那个微型服务并且只能通过它的API访问。封装数据可以确保微服务是松散耦合的,并且可以相互独立地进化。如果多个服务访问相同的数据,架构更新需要对所有服务进行耗时、协调的更新。
更糟糕的是,不同的微服务经常使用不同类型的数据库。现代应用程序存储和处理各种数据和关系数据库并不总是最佳选择。对于某些用例,特定的NoSQL数据库可能具有更方便的数据模型,并提供更好的性能和可伸缩性。例如,存储和查询文本的服务使用文本搜索引擎(如Elasticsearch)是有意义的。类似地,存储社交图形数据的服务应该使用图形数据库,比如Neo4j。因此,基于微服务的应用程序通常混合使用sql和nosql数据库,即所谓的多标记持久性接近。
用于数据存储的分区的、多标记的持久性体系结构有许多好处,包括松散耦合的服务以及更好的性能和可伸缩性。然而,它确实带来了一些分布式数据管理方面的挑战。
第一个挑战是如何实现跨多个服务保持一致性的业务事务。为了了解为什么这是一个问题,让我们来看看一个在线B2B商店的例子。客户服务维护有关客户的信息,包括他们的信用额度。订单服务管理订单,并必须验证新订单不超过客户的信用限额。在此应用程序的单块版本中,Order Service可以简单地使用ACID事务来检查可用的信用并创建订单。
相反,在微服务体系结构中,Order表和Customer表对于各自的服务是私有的,如下图所示。
订单服务不能直接访问Customer表。它只能使用客户服务提供的API。订单服务可能会使用分布式事务,也称为两阶段提交(2pc)。然而,在现代应用程序中,2PC通常不是一个可行的选择。这,这个,那,那个盖定理要求您在可用性和ACID风格的一致性之间进行选择,而可用性通常是更好的选择。此外,许多现代技术,如大多数NoSQL数据库,都不支持2PC。维护服务和数据库之间的数据一致性至关重要,因此我们需要另一种解决方案。
第二个挑战是如何实现从多个服务检索数据的查询。例如,假设应用程序需要显示客户和他最近的订单。如果Order Service提供了用于检索客户订单的API,那么您可以使用应用程序端连接来检索这些数据。应用程序从CustomerService检索客户,从OrderService检索客户的订单。但是,假设Order Service只支持通过主键查找订单(也许它使用的是只支持基于主键的检索的NoSQL数据库)。在这种情况下,没有明显的方法来检索所需的数据。
对于许多应用程序,解决方案是使用事件驱动体系结构。在此体系结构中,微服务在发生一些值得注意的事情时发布事件,例如更新业务实体时。其他微服务订阅了这些事件。当微服务接收到事件时,它可以更新自己的业务实体,这可能会导致更多事件被发布。
您可以使用事件来实现跨多个服务的业务事务。事务由一系列步骤组成。每个步骤由一个微服务、更新一个业务实体和发布一个触发下一步的事件组成。下面的图表序列显示了在创建订单时如何使用事件驱动的方法检查可用的信用。微服务通过消息代理交换事件。
Order Service创建状态为New的订单,并发布创建的订单事件。
客户服务使用订单创建事件,为订单保留信用,并发布信用预留事件。
订单服务将使用信用保留事件,并将订单状态更改为打开。
更复杂的场景可能涉及其他步骤,例如在检查客户信用时保留库存。
只要(A)每个服务原子地更新数据库并发布一个事件(稍后会有更多的消息)和(B)MessageBroker保证事件至少交付一次,那么您就可以实现跨多个服务的业务事务。必须指出的是,这些不是ACID交易。他们提供的担保要弱得多,比如最终一致性。此事务模型被称为基模型.
您还可以使用事件来维护由多个微服务拥有的预联接数据的物化视图。维护视图的服务订阅相关事件并更新视图。例如,维护CustomerOrders视图的CustomerOrderViewUpdaterService订阅由CustomerService和OrderService发布的事件。
当Customer Order View Updater Service收到客户或订单事件时,它会更新Customer Order View数据存储。您可以使用文档数据库(如MongoDB)实现CustomerOrderView,并为每个客户存储一个文档。CustomerOrderView查询服务通过查询CustomerOrderView数据存储来处理对客户和最近订单的请求。
事件驱动的体系结构有几个优点和缺点。它支持跨多个服务并提供最终一致性的事务的实现。另一个好处是它还允许应用程序维护物化视图。一个缺点是编程模型比使用ACID事务时更加复杂。通常,您必须实现补偿事务以从应用程序级故障中恢复;例如,如果信用检查失败,则必须取消订单。此外,应用程序必须处理不一致的数据。这是因为飞行中的事务所做的更改是可见的。如果应用程序从尚未更新的物化视图中读取,也会看到不一致。另一个缺点是订阅者必须检测并忽略重复事件。
在事件驱动的体系结构中,也存在着原子更新数据库和发布事件的问题。例如,Order Service必须向Order表中插入一行并发布创建的订单事件。这两个操作必须以原子方式完成。如果服务在更新数据库后但在发布事件之前崩溃,则系统将变得不一致。确保原子性的标准方法是使用涉及数据库和消息代理的分布式事务。然而,由于上述原因,如CAP定理,这正是我们不想做的。
实现原子性的一种方法是应用程序使用只涉及本地事务的多步处理。诀窍是在存储业务实体状态的数据库中有一个作为消息队列的事件表。应用程序启动(本地)数据库事务,更新业务实体的状态,将事件插入事件表,并提交事务。单独的应用程序线程或进程查询事件表,将事件发布到MessageBroker,然后使用本地事务将事件标记为已发布的事件。下图显示了设计。
Order Service将一行插入订单表,并将创建的订单事件插入事件表。事件发布线程或进程查询未发布事件的事件表,发布事件,然后更新事件表,将事件标记为已发布的事件。
这种方法有几个优点和缺点。一个好处是,它可以保证为每个更新发布一个事件,而不依赖于2pc。此外,应用程序发布业务级别的事件,这消除了推断它们的需要。这种方法的一个缺点是它可能容易出错,因为开发人员必须记住发布事件。这种方法的一个限制是在使用某些NoSQL数据库时很难实现,因为它们的事务和查询功能有限。
这种方法通过让应用程序使用本地事务来更新状态和发布事件,从而消除了对2PC的需求。现在让我们来看看一种通过让应用程序简单地更新状态来实现原子性的方法。
另一种在没有2PC的情况下实现原子性的方法是,事件由挖掘数据库事务或提交日志的线程或进程发布。应用程序更新数据库,从而在数据库的事务日志中记录更改。事务日志Miner线程或进程读取事务日志并将事件发布到MessageBroker。下图显示了设计。
这种方法的一个例子是开放源码。LinkedIn数据库项目。数据库挖掘Oracle事务日志并发布与更改相对应的事件。LinkedIn使用Databus使各种派生数据存储与记录系统保持一致。
另一个例子是AWS DynamoDB中的流机制,它是托管的NoSQL数据库。DynamoDB流包含过去24小时内对DynamoDB表中的项进行的按时间顺序的更改序列(创建、更新和删除操作)。应用程序可以从流中读取这些更改,例如,将它们发布为事件。
事务日志挖掘有不同的优缺点。一个好处是它可以保证在不使用2pc的情况下为每个更新发布一个事件。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用程序。一个主要的缺点是事务日志的格式对每个数据库都是专有的,甚至可以在不同的数据库版本之间进行更改。此外,很难从事务日志中记录的低级更新中反向工程高级业务事件。
事务日志挖掘通过让应用程序做一件事来消除对2PC的需求:更新数据库。现在让我们看一种不同的方法,它消除了更新,只依赖于事件。
事件来源通过使用一种完全不同的、以事件为中心的方法来持久化业务实体,从而实现无需2PC的原子性。应用程序不是存储实体的当前状态,而是存储一系列状态更改事件。应用程序通过重放事件重新构造实体的当前状态。每当业务实体的状态发生变化时,都会将新事件追加到事件列表中。因为保存一个事件是一个单一的操作,所以它本质上是原子的。
要查看事件源是如何工作的,请以Order实体为例。在传统方法中,每个订单映射到Order表中的一行和行,例如Order_line_Item表中的行。但在使用事件源时,Order Service以其状态更改事件的形式存储订单:创建、批准、发送、取消。每个事件包含足够的数据来重建订单的状态。
事件在事件库中持久化,这是一个事件数据库。存储有一个API,用于添加和检索实体的事件。在我们前面描述的体系结构中,EventStore的行为也类似于MessageBroker。它提供了一个API,使服务能够订阅事件。事件存储将所有事件传递给所有感兴趣的订阅者。事件存储是事件驱动的微服务体系结构的主干。
事件来源有几个好处。它解决了实现事件驱动体系结构的关键问题之一,并使每当状态更改时能够可靠地发布事件。因此,它解决了微服务体系结构中的数据一致性问题。而且,因为它保存事件而不是域对象,所以它基本上避免了对象-关系阻抗失配问题。事件源还提供了对业务实体所做更改的100%可靠的审计日志,并使实现在任何时间点确定实体状态的时态查询成为可能。事件来源的另一个主要好处是,您的业务逻辑由松散耦合的业务实体组成,这些实体可以交换事件。这使得从单块应用程序迁移到微服务体系结构更加容易。
事件源也有一些缺点。这是一种不同的和不熟悉的编程风格,因此有一个学习曲线。事件存储仅直接支持通过主键查找业务实体。你必须用命令查询责任分离(CQRS)实现查询。因此,应用程序必须最终处理一致的数据。
在微服务体系结构中,每个微服务都有自己的私有数据存储。不同的微服务可能使用不同的SQL和NoSQL数据库。虽然这种数据库架构有很大的好处,但它也带来了一些分布式数据管理方面的挑战。第一个挑战是如何实现跨多个服务保持一致性的业务事务。第二个挑战是如何实现从多个服务检索数据的查询。
对于许多应用程序,解决方案是使用事件驱动的体系结构。实现事件驱动体系结构的一个挑战是如何原子地更新状态和如何发布事件。有几种方法可以实现这一点,包括将数据库用作消息队列、事务日志挖掘和事件源。
在未来的博客文章中,我们将继续深入研究微服务的其他方面。