当前位置: 首页 > 后端技术 > Java

如何构建DDD驱动的微服务?

时间:2023-04-01 21:28:44 Java

虽然微服务中的“微”字表示服务的规模,但并不是使用微服务的唯一标准。当团队转向基于微服务的架构时,他们的目标是提高敏捷性并自主且频繁地部署功能。这种架构风格的简单定义很难确定。我喜欢AdrianCockcroft对微服务的简短定义:“一种面向服务的架构,由松散耦合的、受上下文限制的元素组成。”虽然这定义了高级设计启发式,但微服务架构具有一些独特的特性,使其不同于以前的面向服务的架构。以下是其中一些特征。这些文档和其他几个文档都有很好的记录——MartinFowler的文章和SamNewman的构建微服务,仅举几例。服务围绕业务上下文有明确定义的边界,而不是任意的技术抽象。例如,不共享数据库。服务对故障具有弹性。团队独立拥有职能,并能够自主发布变更。团队拥抱自动化文化。例如,自动化测试、持续集成和持续交付简而言之,我们可以将这种架构风格总结如下:一种松散耦合的面向服务的架构,其中每个服务都包含在一个定义良好的有界上下文中,这使得快速、频繁可靠地交付应用程序。领域驱动设计和限界上下文微服务的力量来自于明确定义它们的职责和划定它们之间的界限。这里的目的是建立边界内高内聚,边界外低耦合(banq注:高内聚低耦合)。也就是说,倾向于一起变化的事物应该属于同一事物。与许多现实生活中的问题一样,这说起来容易做起来难,随着业务的发展,逻辑假设也在发展。因此,重新配置的能力是设计系统时要考虑的另一个关键问题。领域驱动设计(DDD)是关键,在我们看来,它是设计微服务时必不可少的工具,无论是分解单体还是实施新项目。领域驱动设计(EricEvans在他的书中)是一组思想、原则和模式,有助于基于业务领域的底层模型设计软件系统。开发人员和领域专家一起工作,以共同的通用语言创建业务模型。然后,他们将这些模型绑定到有意义的系统,并在这些系统和从事这些服务的团队之间建立协作协议。更重要的是,他们设计系统之间的概念轮廓或边界。微服务设计从这些概念中汲取灵感,因为所有这些原则都有助于构建可以相互独立改变和发展的模块化系统。在我们继续之前,让我们快速浏览一下DDD的一些基本术语。领域驱动设计的完整概述超出了本博客的范围。我们向所有尝试构建微服务的人强烈推荐EricEvans的书籍。领域:代表组织的工作。例如,它是零售或电子商务。子域:组织或组织内的业务单元。一个域由多个子域组成。通用语言:这是用于表达模型的语言。在下面的示例中,Item是属于每个子域的通用语言的模型。开发人员、产品经理、领域专家和业务利益相关者都同意使用相同的语言并将其用于他们的工件(代码、产品文档等)。限界上下文:领域驱动设计将限界上下文定义为“一组含义已确定的单词或句子”。简而言之,这意味着模型是一个有意义的边界。在上面的示例中,“项目”在每个上下文中表示不同的事物。在目录的上下文中,项目表示可供销售的产品,在购物车的上下文中,它表示客户已添加到购物车的项目。在Shipping上下文中,它表示将运送给客户的仓库项目。这些模型中的每一个都是不同的,每个都有不同的含义并且可能包含不同的属性。通过在各自的边界内分离和隔离这些模型,我们可以毫无歧义地自由表达模型。注意:了解子域和限界上下文之间的区别很重要。子域属于问题空间,你的业务如何看待问题,有界上下文属于解决方案空间,我们将如何实施解决问题的方案。理论上每个子域可以有多个限界上下文,尽管我们努力为每个子域提供一个限界上下文。微服务如何与限界上下文相关现在,微服务适用于哪些领域?是否可以说每一个限界上下文都映射到一个微服务?是的,我们会明白为什么。在某些情况下,有界上下文的边界或轮廓可能很大。考虑上面的例子。定价绑定上下文有三个不同的模型——Price、PricingItem和Discount,每个模型负责一个目录项的价格,分别计算项目列表的总价和应用折扣。我们可以创建一个包含所有上述模型的系统,但它可能会变成一个不合理的大型应用程序。如前所述,每个数据模型都有其不变量和业务规则。随着时间的推移,如果我们不小心,系统可能会变成一个边界模糊、责任重叠的大泥球,甚至可能回到我们开始的地方——作为一个整体。另一种对系统建模的方法是将相关模型分离或分组为单独的微服务。在DDD中,这些模型(价格、定价项目和折扣)称为聚合。聚合是组成相关模型的独立模型。您只能通过已发布的接口更改聚合的状态,并且聚合可确保一致性和不变量保持良好状态。聚合是被视为数据更改单元的关联对象的集群。外部引用仅限于AGGREGATE的一个成员,称为根。一组适用于AGGREGATE边界的一致性规则。此外,没有必要将每个聚合建模为不同的微服务。图中的服务(聚合)是一对一的关系,但这不一定是规则。在某些情况下,在单个服务中托管多个聚合可能有意义,尤其是当我们不完全了解业务领域时。需要注意的重要一点是,一致性只能在单个聚合内得到保证,聚合只能通过已发布的接口进行修改。任何违反这些规则的行为都有可能变成一个大麻烦。上下文映射——一种精确划定微服务边界的方法整体结构通常由不同的模型组成,其中大部分是紧密耦合的——模型可能知道彼此的亲密细节,改变一个模型可能会对另一个模型产生副作用,这取决于等等。在分解整体时,识别这些模型(在本例中为集合)及其关系至关重要。上下文映射可以帮助我们做到这一点。它们用于识别和定义各种有界上下文和聚合之间的关系。在上面的示例中,有界上下文定义了模型的边界(价格、折扣等),上下文映射定义了这些模型之间以及不同有界上下文之间的关系。对上下文映射的全面探索超出了本博客的范围,但我们将通过示例进行说明。下图显示了处理电子商务订单付款的各种应用程序。购物车上下文负责订单的在线授权;订单上下文在发布付款后处理付款流程;联络中心处理所有异常情况,例如重试付款和更改用于订单的付款方式。为简单起见,让我们假设所有这些上下文所有这些上下文作为单独的服务实现,封装相同的模型。请注意,这些模型在逻辑上是相同的。也就是说,它们都遵循相同的通用领域语言——支付方式、授权和计费。只是它们属于不同的上下文。重新定义服务边界——将聚合映射到正确的上下文。错误情况如下图所示:电子商务中的所有模型都直接与单个支付聚合的支付网关上下文集成。支付需要是事务性的,但是由于多种服务的集成,支付的事务性不能通过强制各个服务之间的不变性和一致性来实现,(banq注:当然有人提出了分布式事务的概念来实现事务这些不同服务性之间的支付流程,这实际上是基于错误设计的伪概念)。请注意,支付网关中的任何更改都将强制更改多个服务,并可能更改多个团队,因为不同的组可以拥有这些上下文。通过一些调整并将聚合与正确的上下文对齐,我们可以更好地表示这些子域,如下所示。很多都变了。让我们回顾一下这些变化:通常,整体式(monolithic)或遗留应用程序有许多聚合,通常具有重叠的边界。创建这些聚合及其依赖关系的上下文映射有助于我们了解将从这些整体中出现的任何新微服务的轮廓。请记住,微服务架构的成功或失败取决于聚合之间的低耦合和这些聚合之间的高内聚。同样重要的是要注意,限界上下文本身就是一个合适的内聚单元。即使一个上下文有多个聚合,整个上下文和它的聚合也可以形成一个微服务。我们发现这种启发式方法对于有些晦涩的领域特别有用——考虑组织正在涉足的新业务领域。您可能对正确的分离边界没有足够的了解,任何聚合的过早分解都可能导致代价高昂的重构。想象一下,由于数据迁移而不得不将两个数据库合并为一个,因为我们偶然发现了属于同一类的两个聚合。但要确保这些聚合通过接口充分隔离,以至于它们不知道彼此的复杂细节。事件风暴——识别服务边界的另一种技术事件风暴是识别系统中的聚合(以及微服务)的另一种基本技术。这对于破坏整体结构以及设计复杂的微服务生态系统都是一个有用的工具。我们已经使用这种技术分解了一个复杂的应用程序,并打算在单独的博客中介绍事件风暴的经验。对于本博客的范围,我们想提供一个快速的高级概述。如果您有兴趣进一步探索,请观看AlbertoBrandelloni的视频。简而言之,事件风暴是在应用程序团队(在我们的案例中作为一个整体)内发生的头脑风暴,以识别系统中发生的各种领域事件和过程。该团队还确定这些事件的影响及其后续影响的汇总或模型。当团队完成这个练习时,他们会识别出不同的重叠概念、模棱两可的领域语言和相互冲突的业务流程。他们对相关模型进行分组,重新定义聚合并识别重复过程。随着这项工作的进行,这些集合所属的有界上下文变得清晰起来。如果所有团队都在同一个房间(物理或虚拟)并开始在Scrum风格的白板上绘制事件、命令和流程的映射,那么事件风暴研讨会就很棒。在本练习结束时,重新定义聚合列表。这些可能成为新的微服务,需要在直接从其他应用程序或用户调用的微服务命令之间流动域事件我们在事件风暴研讨会结束时展示了一个示例板。对于团队来说,这是一项很好的协作活动,可以就正确的聚合和有限的上下文达成一致。除了进行出色的团队建设练习外,团队在本次会议结束后对领域、通用语言和精确的服务边界有了共同的理解。微服务之间的通信单体在进程边界内托管多个聚合。因此,在此边界内,可以管理聚合的事务一致性,例如如果客户下订单,我们可以清仓商品并向客户发送电子邮件,所有这些都在一次交易中完成。要么所有操作都成功,要么全部失败。然而,当我们分解整体并将聚合分散到不同的环境中时,我们将拥有数十个甚至数百个微服务。迄今为止存在于单体单一边界内的进程现在分布在多个分布式系统中。在所有这些分布式系统上实现事务的完整性和一致性是非常困难的,而且要付出代价——系统的可用性。微服务也是分布式系统。因此,CAP定理也适用于它们——“分布式系统只能提供三个必需特性中的两个:一致性、可用性和分区容错性(CAP中的“C”、“A”和“P”)。”在现实世界的系统中,分区容忍度是不可协商的——网络不可靠,虚拟机可能会宕机,区域之间的延迟会变得更糟等等。因此,我们可以选择“可用性”或“一致性”。现在,我们知道牺牲可用性在任何现代应用程序中也不是一个好主意。围绕最终一致性设计您的应用程序如果您尝试跨多个分布式系统构建事务,您将再次陷入困境。成为最糟糕的分布式整体事务。如果有任何人系统的某个点不可用,整个过程将不可用,通常会导致令人沮丧的客户体验,失去保证失败承诺等。此外,对一项服务的更改可能通常需要更改另一项服务,从而导致复杂且昂贵的因此,最好根据我们的用例设计应用程序以容忍一点点不一致以提高可用性。对于上面的示例,我们可以使所有进程异步,以便它们最终变得一致。我们可以独立于其他进程异步发送电子邮件。如果承诺的商品后来在仓库中不可用,则该商品可能会延期交货,或者我们可能会停止接受超过特定阈值的商品的订单。有时,您可能会遇到这样一种情况,即在跨越不同进程边界的两个聚合中需要强ACID样式的事务。这是重新审视这些聚合并将它们合二为一的好兆头。事件风暴和上下文映射将有助于在我们开始在不同流程边界中分解这些聚合之前尽早识别这些依赖关系。将两个微服务合二为一成本高昂,我们应该尽量避免。支持事件驱动架构的微服务可能会在其聚合中发出内在变化。这些称为域事件,任何对这些更改感兴趣的服务都可以侦听这些事件并在其域内采取相应的行动。这种方法避免了任何行为耦合:一个域不指示其他域应该做什么,以及时间耦合——一个过程的成功完成不依赖于所有系统同时可用。当然,这将意味着系统最终会保持一致。在上面的例子中,订单服务发布了一个事件——订单取消。订阅该事件的其他服务处理它们各自的域功能:支付服务退还款项,库存服务调整物品的库存,等等。确保此集成的可靠性和弹性的几点注意事项:生产者应确保至少生产一个事件。如果失败,您应该确保存在回退机制来重新触发事件。消费者应确保以幂等方式消费事件。如果同样的事件再次发生,那么对用户端不应该有任何副作用。事件也可能乱序到达。消费者可以使用时间戳或版本号字段来保证事件的唯一性。由于某些用例的性质,可能并不总是可以使用基于事件的集成。查看购物车服务和支付服务之间的集成。这是同步集成,因此我们需要注意一些事项。这是行为耦合的一个示例-购物车服务可能会从支付服务调用RESTAPI并指示它授权订单支付,而时间耦合将要求购物车服务使用支付服务来接受订单。这种耦合减少了这些上下文的自主性,并可能减少不需要的依赖性。有几种方法可以避免这种耦合,但是有了所有这些选项,我们就失去了向客户提供即时反馈的能力。将RESTAPI转换为基于事件的集成。但是,如果支付服务仅公开RESTAPI,则此选项可能不可用。购物车服务立即接受订单,并且有一个批处理作业接收订单并调用支付服务API。购物车服务产生一个本地事件,然后调用支付服务。将以上内容与重试相结合,可以在API失败且上游依赖项(支付服务)不可用的情况下实现更具弹性的设计。例如,购物车和支付服务之间的同步集成可以通过事件或基于批处理的重试进行备份,以防失败。这种方法对客户体验有额外的影响:客户可能输入了错误的付款详细信息,而当我们离线处理付款时,我们不会将它们带到网上。否则,恢复失败的付款会增加业务成本。然而,购物车服务对支付服务不可用或故障的恢复能力很可能超过其缺点。例如,如果我们无法离线收款,我们可以通知客户。避免针对特定消费者数据需求的服务之间的编排任何面向服务的体系结构中的反模式之一是服务都可以满足消费者的特定访问模式。通常,当消费者团队与服务??团队密切合作时,就会发生这种情况。如果团队正在开发单体应用程序,他们通常会创建一个跨越不同聚合边界的API,从而将聚合紧密耦合。让我们考虑一个例子。在网络中说“订单详情”页面,移动应用程序需要在单个页面上同时显示订单详情和为该订单处理的退款详情。在单体应用程序中,OrderGETAPI(假设它是一个RESTAPI)同时查询Orders和Refunds,合并这两个聚合,并将复合响应发送给调用者。这可以在没有太多开销的情况下完成,因为聚合属于相同的进程边界。因此,消费者可以在一次调用中获得所有必要的数据。如果订单和退款是不同上下文的一部分,则数据将不再存在于单个微服务或聚合边界内。为消费者保留相同功能的一种选择是让订单服务负责调用退款服务并创建复合响应。这种方法会导致以下问题:订单服务现在与另一个服务集成,纯粹是为了支持需要退款数据和订单数据的消费者。订单服务现在的自主性较低,因为退款总额的任何变化都会导致订单总额的变化。订单服务有另外一个集成,所以要考虑另一个失败点——如果退款服务失败,订单服务是否还能发送一些数据,消费者优雅地失败?如果消费者需要更改以从“退款”聚合中获取更多数据,现在需要两个团队来进行更改。服务满足调用者的特定访问模式。前端的后端BFF减轻这种风险的一种方法是让消费者团队管理各种域服务之间的编排。毕竟,调用者会对访问模式有更好的了解,并且可以完全控制对这些模式的任何更改。这种方法将领域服务与表示层分开,使它们能够专注于核心业务流程。然而,如果Web和移动应用程序开始直接调用不同的服务,而不是从整体调用一个复合API,则可能会导致这些应用程序的性能开销——通过低带宽网络进行多次调用,处理和合并来自不同API数据等。相反,可以使用另一种称为前端后端的模式。在这种设计模式中,由消费者(在本例中为Web和移动团队)创建和管理的后端服务负责跨多个域的服务集成,纯粹是为了向客户提供前端体验。Web和移动团队现在可以根据他们的用例设计数据合同。他们甚至可以使用GraphQL而不是RESTAPI来灵活查询并准确获取他们需要的信息。请务必注意,此服务由消费者团队拥有和维护,而不是拥有域服务的团队。前端团队现在可以根据他们的需要进行优化——移动应用程序可以请求更小的有效载荷,减少来自移动应用程序的调用次数等。查看下面业务流程的修订视图。结论在这篇博客中,我们谈到了在进入微服务领域时要考虑的各种概念、策略和设计启发式方法,尤其是在尝试将单体服务拆分为基于域的微服务时。其中许多主题本身就是广泛的主题,我认为我们没有充分公正地对它们进行详细解释,但我们想介绍一些关键主题以及我们采用它们的经验。进一步阅读(链接)部分为希望采用此方法的任何人提供了一些参考资料和一些有用的内容。原文:https://medium.com/walmartglo...\译文:jdon.com/54558近期文章推荐:1.1,000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!