微服务业务开发三个难题-拆分、事务、查询(下)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介:

上集我们阐述了使用微服务体系架构的关键障碍是领域模型,事务和查询,这三个障碍似乎和功能拆分具有天然的对抗。只要功能拆分了,就涉及这三个难题。


然后我们向你展示了一种解决方案就是将每个服务的业务逻辑实现为一组DDD聚合。然后每个事务只能更新或创建一个单独的聚合。然后通过事件来维护聚合(和服务)之间的数据一致性。


在本集中,我们将会向你介绍使用事件的时候遇到了一个新的问题,就是怎么样通过原子方式更新聚合和发布事件。然后会展示如何使用事件源来解决这个问题,事件源是一种以事件为中心的业务逻辑设计和持久化的方法。之后,我们会阐述微服务架构下的查询困难的问题。然后向你介绍一种称为命令查询责任分离(CQRS的方法来实现可扩展和高性能的查询。

 

可靠地更新状态和发布事件

 

从表面上看,使用事件来保持聚合之间的一致性似乎很简单。


当一个服务创建或更新数据库的一个聚合时,它只是简单地发布一个事件。

但是,这只是表象,其实还有一个核心问题就是:更新数据库和发布事件必须是原子的。否则,就会出现类似这样的情况:如果服务在更新数据库之后但在发布事件之前崩溃,则系统就出现了不一致的问题。

 

传统的解决方案是一般都是使用分布式事务来搞,一个涉及数据库和消息broker的分布式事务。但是,由于上一集所述的原因,2PC不是一个可行的选择。

其实除了2PC ,还有几种解决这个问题的方法。


一种解决方案就是,应用程序可以通过向类似Kafka这样的消息中间件的broker发布一个事件来执行更新。然后一个消息consumer订阅这个事件,通过消费该事件然后最终更新数据库。这种方法可以确保数据库被更新并且事件被发布。

但是缺点就是这种一致性模型过于复杂,至少有点复杂。而且应用程序不能够立即读取到自己刚刚的写入。


1 - 通过发布事件到消息broker来更新数据库

 

另一种做法就是,如图2所示,就是应用程序追加事务日志到数据库(a.k.a.commit log),将每个记录的更改转换为事件,然后把事件发布到消息broker。这种做法的一个重要好处就是应用程序本身不需要任何的改变。

 

然而,一个缺点是,这种做法是一种底层(low-level)的事件,而不是上层业务事件。可能难以将上层业务事件(由于数据库更新的原因)从底层更改逆转到表中的行。

 

原文:it can be difficult toreverse engineer the high-level business event - the reason for the databaseupdate - from the low-level changes to the rows in the tables.

 

wKioL1jkp5bwYYG0AAANZ93X4q8271.jpg


2 - 追加数据库事务日志

 

第三种解决方案就是,图3所示的这种,使用数据库表来作为一种临时性的message queue。当一个服务更新一个聚合,它会insert一个事件到EVENTS表,作为本地ACID事务的一部分。然后一个单独的进程轮询EVENTS表并将事件发布到消息broker


这种做法的好处就是service能够发布high-level的业务事件。


缺点是这种做法容易出错,有这种潜在的可能,因为事件发布代码必须与业务逻辑同步。


wKioL1jkp93gxI1FAAAPjmZaj1U903.jpg

3 - 使用数据库表作为message queue


上面三种做法都有比较典型的缺点。


发布一个事件到message broker并稍后更新的做法总是不能提供一种read-your-writes的一致性,也就是只能保证最终一致。


追加事务日志提供了一致的读取,但却不能发布高级业务事件。


使用数据库表作为message queue提供了一致的读取并且可以发布high-level业务事件,但

却对开发人员有依赖,就是开发人员得记得在状态发生改变的时候加上发布事件的逻辑。

 

幸运的是,我们还有另外一种解决方案,那就是event sourcing,事件源。它是一种针对持久化和业务逻辑的一种以事件为中心方法,称为事件源。这里解释的不够清楚,稍后慢慢展开。

 

使用事件源来开发微服务

 

事件源(Event sourcing)是一种以事件为中心的持久化方法。这不是一个新的概念。

 

我第一次了解到这个概念是在大概五年多以前,之后对这个新生事物一直充满了好奇,直到我开始开发微服务。接下来,你将会看到通过事件源来实现事件驱动的微服务架构是多么不错的一种方法。

 

一个service通过event sourcing使用一系列的事件来持久化每个聚合。

当创建或更新一个聚合的时候,这个service会在数据库里保存一个或多个事件,这种数据库里存储event的方式可以叫做是event store,以下我们就叫事件数据库


它通过加载这些事件并replay这些事件,从而实现更新聚合的当前状态。

在函数式编程里,一个service通过执行一个函数式的foldreduce来重构聚合,而不是事件。


由于事件就是状态,所以你就不会再有原子地更新状态和发布事件的问题了。


例如,比如订单服务(Order Service)。不是将每个订单作为一行存储在ORDERS表中,而是将每个订单聚合作为一系列的事件,比如订单已创建,订单已批准,订单已发货等持久化到EVENTS表中。图4显示了这些事件如何存储在基于SQL的事件数据库(event store)中。


wKiom1jkqA_BlsadAAA22ieHK7M335.jpg

4 - 使用事件源来持久化一个订单


每列的意思:

  • entity_type entity_id –唯一标识一个聚合

  • event_id – 事件ID,唯一标识

  • event_type – 事件类型

  • event_data -事件属性的序列化JSON表示

 

一些事件包含大量数据。例如,订单创建(Order Created)事件包含完整订单,包括其订单项,付款信息和交货信息。其他事件,如订单出货(Order Shipped)事件,包含很少或没有数据,只是表示状态转换。

 

事件源(Event Sourcing)和发布事件


严格的讲,事件源只是简单的将聚合们作为事件进行了持久化。更直接的说,就是使用事件源来作为一种可靠的事件发布机制。保存一个事件是一个固有的原子操作,它可以确保事件数据库(event store)把事件传递给感兴趣的服务。

 

例如,如果事件被存储在上面所示的EVENTS表中,订阅者可以简单地轮询表以查找新事件。更复杂的事件数据库(event store)将使用另一种做法,这种做法具有更高性能和可扩展性。例如,Eventuate Local使用追加事务日志的方式。它从MySQL replication流中读取插入到EVENTS表中的事件,并将它们发布到Apache Kafka


至于Eventuate Local是个什么鬼?你可以去github 搜搜。下面放一张图:

wKiom1jkqDugBIgkAAC6YRU-MF8990.jpg

 

使用Snapshot改善性能


订单(Order)聚合具有相对较少的状态转换,因此它只有少量的事件。

所以,针对这些事件查询事件数据库(event store)并重构Order聚合,效率是不错的。然而,一些聚合有很多的事件。例如,客户(Customer)聚合可能有大量的预留信用(Credit Reserved)事件。随着时间的推移,加载和消费(fold)这些事件的效率会越来越低。


一个常见的解决方案是定期保存聚合状态的快照(snapshot)。应用程序通过加载最近的快照然后从快照创建之后发生的那些事件开始来恢复聚合的状态。

在函数式下,快照就是折叠(fold)的初始值。(原文:In functional terms, the snapshot is the initial value of thefold. 如果聚合是一个简单,容易序列化的结构,则快照可以简单地是JSON序列化格式。更复杂的聚合可以使用Memento模式(Mementopattern)进行快照。至于这种设计模式具体是什么鬼,你可以自己查阅。

 

在线商店示例中的客户(Customer)聚合具有非常简单的结构:客户的信息,他们的信用额度(credit limit)和他们的信用预留(credit reservations)。

客户(Customer)的快照只是其状态的JSON序列化。图5展现了如何从与事件#103的客户(Customer)的状态相对应的快照中重新创建一个客户(Customer)。客户服务(Customer Service)只需要加载快照和加载事件#103后发生的事件。


wKioL1jkqGSBf0KoAAAjN1_j4kE184.jpg

5 – 使用快照来优化性能


客户服务(Customer Service)通过反序列化快照的JSON后加载并消费#104到#106的事件来重新创建那个客户(Customer)。


事件源实现


事件数据库(event store)是数据库和消息borker的混合体。它是一个数据库,因为它有一个API,用于通过主键插入和检索聚合的事件。事件数据库(event store)也是消息broker,因为它具有用于订阅事件的API


有一些不同的方法来实现事件数据库(event store)。


一个做法是编写自己的事件源框架。例如,您可以在RDBMS中持久化事件。一种简单的,但性能略低的方式来发布事件,然后订阅者轮询事件的EVENTS表。


另一个做法是使用专用的事件数据库(event store,它通常能够提供更丰富的功能以及更好的性能和可扩展性。事件源的开发者之一Greg Young有一个基于.NET的开源事件数据库,称为Event Store Lightbend,这个公司以前叫Typesafe,有一个叫Lagom的微服务框架,是基于事件源的。这里推荐一个我自己的创业项目,Eventuate,一个用于微服务的事件源框架,你可以把它作为一个云服务,你也可以把它认为是一个基于Kafka RDBMS的开源项目。

 

 

事件源的好处与缺点


事件源有好处也有缺点。


事件源的一个主要优点是它可以在聚合的状态发生变化时可靠地发布事件。它为事件驱动的微服务架构打下了良好的基础。而且,由于每个事件都可以记录进行更改的用户的身份,因此事件源还提供了一个准确的审核日志。事件流可用于各种其他目的,包括向用户发送通知以及应用集成等等。


事件源的另一个好处是它存储每个聚合的整个历史。你可以轻松实现检索聚合的过去状态的时态查询。要确定在给定时间点的聚合的状态,您只需消费(fold)直到该点为止发生的事件。例如,可以直接计算过去某个时间点客户的可用信用额。

 

事件源也避免了O / R阻抗失衡的问题。这是因为它持久化了事件而不是聚合。事件通常具有简单,容易序列化的结构。服务(service)可以通过序列化其状态的记录来对复杂聚合进行快照。 Memento模式在聚合和它的序列化表示之间增加了一个中间层。


有关O/R impedance mismatch:

对象关系阻抗失衡(object-relational impedance mismatch )是当关系数据库管理系统(RDBMS)由以面向对象的编程语言或风格编写的应用程序(或多个应用程序)服务时经常遇到的一组概念和技术困难,特别是因为对象或类定义必须映射到关系模式定义的数据库表。

 

事件源当然不是完美的,它也有一些缺点。它是一个完全不一样的和而且你可能并不熟悉的编程模型,所以要花一些时间去学习。为了使现有应用程序使用事件源,你必须要重写业务逻辑。幸运的是,这是一个相当机械的转换,你可以在将应用程序迁移到微服务的时候做这件事情。

 

事件源的另一个缺点是消息broker通常保证至少一次(at-least once)传递。非幂等的事件处理handler必须检测并丢弃那些重复的事件。事件源框架可以通过为每个事件分配单调递增的id来解决这个问题。事件处理handler然后可以通过对最大事件ID跟踪来检测重复事件。

 

事件源的另一个局限就是事件(和快照!)的schema将随时间发展。 由于事件永久存储,当服务重建聚合时,服务可能需要折叠与多个schema版本对应的事件。 简化服务的一种方法是,当事件源框架从事件数据库(event store)加载它们时,将所有事件转换为最新版本的模式。因此,服务只需消费(fold)最新版本的事件。

 

事件源的另一个缺点是查询事件数据库(event store)可能比较困难。让我们想象一下,例如,您需要找到信用额度较低的客户。你不能简单地写SELECT * FROM CUSTOMERWHERE CREDIT_LIMIT < AND c.CREATION_DATE>?。因为根本就没有信用额度(CREDIT_LIMIT)这样的列。相反,你不得不使用嵌套SELECT的更复杂而且还可能无效的查询,通过处理和消费(fold)事件来计算信用额度。更糟糕的是,基于NoSQL的事件数据库(event store)通常只支持基于主键的查找。因此,必须使用命令查询责任分离CQRS)的方法实施查询。CQRS 的全称:Command Query Responsibility Segregation


我们接下来的内容就是介绍CQRS


 

使用CQRS实现查询


事件源是在微服务体系结构中实现高效查询的主要障碍。这还不是唯一的问题,还有比如你使用SQL去查找一些高价值订单的新客户。

SELECT *
FROM CUSTOMER c, ORDER o
WHERE
   c.id = o.ID
     AND o.ORDER_TOTAL > 100000
     AND o.STATE = 'SHIPPED'
     AND c.CREATION_DATE > ?

 

在微服务架构中,你不能join CUSTOMERORDER这两张表。每个表由不同的服务所拥有,并且只能通过该服务的API访问。你不能编写连接多个服务所拥有的表的传统查询。事件源使事情变得更糟,阻碍你编写简单,直接的查询。让我们来看看在微服务架构中是如何实现类似查询的。

 

如何使用CQRS


实现查询的好方法是使用称为命令查询责任分离(CQRS)的体系结构模式: Command Query Responsibility Segregation如名称所示,CQRS将应用程序分为两部分。第一部分是命令侧(command-side,其处理命令(例如,HTTP POSTPUTDELETE)以创建,更新和删除聚合。前提是这些聚合是使用事件源实现的。应用程序的第二部分是查询侧(query-side,其通过查询聚合的一个或多个物化视图(materialized views)来处理查询(例如HTTP GET)。查询侧通过订阅由命令侧发布的事件来保持视图(view)与聚合(aggregate)同步。

 

查询侧(query-side)视图可以使用任何类型的能满足需求的数据库来实现。根据需求,应用程序的查询端可能使用一个或多个以下数据库:


1. 查询侧视图数据库选择

wKiom1jkqJnwtEzzAAHx19GSM5M992.jpg

 

在很多场合,CQRS是一个以事件为基础(event-based)的综合体,比如使用RDBMS作为记录系统再使用比如Elasticsearch来处理文本查询。CQRS的查询侧可以使用其它类型的数据库,支持多种类型的数据库,不仅仅是文本搜索引擎。而且,它通过订阅事件准实时地去更新查询侧的视图。


6显示了应用于在线商店示例的CQRS模式。客户服务(Customer Service)和订单服务(Order Service)是命令端服务。它们提供用于创建和更新客户和订单的API。客户视图服务(Customer View Service)是查询侧服务。它提供了一个用于查询客户的API

 

wKioL1jkqL6g9c1lAAAiQyTdb8U121.jpg


6 – 在线商店中使用 CQRS

 

客户视图服务(Customer View Service)订阅命令端服务发布的客户(Customer)和订单(Order)事件。它更新那个用MongoDB实现的视图存储(view store)。该服务维护一个MongoDB文档集合,每个客户一个。每个文档都具有客户详细信息的属性。它还具有存储客户最近订单的属性。此集合支持各种查询,包括上面说到的那些查询。

 

CQRS的好处和缺点


CQRS既有优点也有缺点。 CQRS的一个主要优点是它可以在微服务架构中实现查询,特别是使用事件源的架构。它使应用程序有效地支持一组不同的查询。另一个好处就是把命令侧和查询侧分离,达到了解耦的作用。


CQRS也有一些缺点。一个缺点就是需要额外的工作来开发和维护这套系统。你需要开发和部署更新和查询视图的查询端服务。还有就是你需要部署视图数据库(view store)。


CQRS的另一个缺点是处理命令侧和查询侧视图之间的滞后。查询层相比命令侧存在一定的时延。更新聚合,然后立即查询视图的客户端应用程序可能会看到聚合的以前版本。所以必须通过一些手法来避免暴露这些潜在的不一致性给用户。

 


总结


使用事件来维护服务之间的数据一致性时的主要挑战是原子级地更新数据库和发布事件。传统的解决方案是使用跨数据库和消息broker的分布式事务。然而,2PC不是现代应用的可行技术。更好的方法是使用事件源,这是一种以事件为中心的方法来处理业务逻辑设计和持久化。


微服务架构中的另一个挑战是查询。查询通常需要join由多个服务拥有的数据。但是,join不能再使用了,因为数据对每个服务都是私有的。使用事件源还使得更加难以有效地实现查询,因为当前状态没有被显式地存储。解决方案是使用命令查询责任分离(CQRS)并维护可以容易查询的聚合的一个或多个物化视图。




      本文转自yushiwh 51CTO博客,原文链接:http://blog.51cto.com/yushiwh/1907071,如需转载请自行联系原作者





相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
15天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
18 0
|
29天前
|
负载均衡 测试技术 持续交付
高效后端开发实践:构建可扩展的微服务架构
在当今快速发展的互联网时代,后端开发扮演着至关重要的角色。本文将重点探讨如何构建可扩展的微服务架构,以及在后端开发中提高效率的一些实践方法。通过合理的架构设计和技术选型,我们可以更好地应对日益复杂的业务需求,实现高效可靠的后端系统。
|
1月前
|
监控 Kubernetes 持续交付
构建高效可扩展的微服务架构:后端开发实践指南
在数字化转型的浪潮中,企业对软件系统的要求日益提高,追求快速响应市场变化、持续交付价值成为核心竞争力。微服务架构以其灵活性、模块化和独立部署的特点,成为解决复杂系统问题的有效途径。本文将深入探讨如何构建一个高效且可扩展的微服务架构,涵盖关键设计原则、技术选型及实践案例,为后端开发者提供一条清晰的指导路线,帮助其在不断变化的技术环境中保持竞争力。
130 3
|
3天前
|
消息中间件 监控 持续交付
构建高效微服务架构:后端开发的进阶之路
【4月更文挑战第20天】 随着现代软件开发的复杂性日益增加,传统的单体应用已难以满足快速迭代和灵活部署的需求。微服务架构作为一种新兴的分布式系统设计方式,以其独立部署、易于扩展和维护的特点,成为解决这一问题的关键。本文将深入探讨微服务的核心概念、设计原则以及在后端开发实践中如何构建一个高效的微服务架构。我们将从服务划分、通信机制、数据一致性、服务发现与注册等方面入手,提供一系列实用的策略和建议,帮助开发者优化后端系统的性能和可维护性。
|
1月前
|
存储 JSON 监控
构建高效微服务架构:后端开发的新趋势
【2月更文挑战第29天】在软件开发的世界中,微服务架构已经成为一种流行且有效的方式来组织和部署应用程序。这种架构模式通过将大型、复杂的应用拆分成一系列小型、自治的服务来提供灵活性和可扩展性。本文将探讨微服务的核心概念,包括其定义、优势、挑战以及如何在现代后端开发中实施微服务架构。我们将通过具体案例分析微服务的实施策略,并讨论如何克服常见的技术障碍,以实现一个高效、可维护的系统。
|
24天前
|
监控 Java 开发者
构建高效微服务架构:后端开发的新范式
在数字化转型的浪潮中,微服务架构以其灵活性、可扩展性和容错性成为企业技术战略的关键组成部分。本文深入探讨了微服务的核心概念,包括其设计原则、技术栈选择以及与容器化和编排技术的融合。通过实际案例分析,展示了如何利用微服务架构提升系统性能,实现快速迭代部署,并通过服务的解耦来提高整体系统的可靠性。
|
30天前
|
监控 数据管理 API
构建高效微服务架构:后端开发的新趋势
在现代软件开发领域,随着业务需求的不断复杂化以及敏捷迭代的加速,传统的单体应用架构逐渐暴露出其局限性。微服务架构作为一种新的解决方案,以其高度模块化、独立部署和可扩展性,正成为后端开发领域的新趋势。本文将探讨微服务架构的核心概念,分析其优势与面临的挑战,并提供实施高效微服务的策略和最佳实践,帮助读者理解如何利用这一架构模式提升系统的可靠性、灵活性和可维护性。
136 5
|
1月前
|
人工智能 运维 监控
构建高性能微服务架构:现代后端开发的挑战与策略构建高效自动化运维系统的关键策略
【2月更文挑战第30天】 随着企业应用的复杂性增加,传统的单体应用架构已经难以满足快速迭代和高可用性的需求。微服务架构作为解决方案,以其服务的细粒度、独立性和弹性而受到青睐。本文将深入探讨如何构建一个高性能的微服务系统,包括关键的设计原则、常用的技术栈选择以及性能优化的最佳实践。我们将分析微服务在处理分布式事务、数据一致性以及服务发现等方面的挑战,并提出相应的解决策略。通过实例分析和案例研究,我们的目标是为后端开发人员提供一套实用的指南,帮助他们构建出既能快速响应市场变化,又能保持高效率和稳定性的微服务系统。 【2月更文挑战第30天】随着信息技术的飞速发展,企业对于信息系统的稳定性和效率要求
|
1月前
|
消息中间件 监控 开发者
构建高效微服务架构:后端开发的新趋势
【2月更文挑战第30天】 在现代软件开发领域,微服务架构已成为实现可扩展、灵活且容错的系统的首选设计。本文深入探讨了构建高效微服务架构的关键步骤和最佳实践,涵盖了从服务划分到部署管理的全过程。我们不仅将分析微服务的优势与挑战,还将提供具体的技术建议和解决方案,以帮助后端开发者有效地构建和优化其系统结构。
|
5天前
|
监控 持续交付 开发者
构建高效微服务架构:后端开发的新趋势
【4月更文挑战第18天】在数字化转型的浪潮中,微服务架构已成为企业提升系统灵活性、加速产品迭代的关键。此文深入探讨了构建高效微服务架构的实践方法,包括服务划分原则、容器化部署、持续集成/持续部署(CI/CD)流程以及监控与日志管理等关键技术点。通过分析具体案例,揭示了微服务在提高开发效率、降低维护成本及促进团队协作方面的显著优势。