关于java:如何构建基于-DDD-领域驱动的微服务

5次阅读

共计 7207 个字符,预计需要花费 19 分钟才能阅读完成。

只管 微服务 中的“微”一词示意服务的规模,但它并不是应用微服务的唯一标准。当团队转向基于微服务的 架构 时,他们旨在进步敏捷性以及自主且频繁地部署性能。很难确定这种架构格调的简略定义。我喜爱 Adrian Cockcroft 的对于微服务的简短定义:“面向服务的体系结构,它由涣散耦合的、具备上下文边界的元素组成。”

只管这定义了高级设计启发式技术,但微服务架构具备一些独特的个性,使其有别于以往的面向服务的架构。以下是其中一些特色。这些以及其余一些文档都有据可查 -Martin Fowler 的文章和 Sam Newman 的 Building Microservices,仅举几例。

  1. 服务具备围绕业务上下文而不是任意技术上形象的明确定义的边界
  2. 通过用意公开界面暗藏实现细节并公开性能
  3. 服务不会共享超出其边界的内部结构。例如,不共享数据库。
  4. 服务能够抵制故障。
  5. 团队独立领有职能,并可能自主公布变更
  6. 团队拥戴自动化文化。例如,自动化测试,继续集成和继续交付

简而言之,咱们能够将这种体系结构款式总结如下:

松耦合的面向服务的体系结构,其中每个服务都蕴含在定义明确的 有界上下文 中,从而能够疾速,频繁且牢靠地交付应用程序。

畛域驱动设计和有界上下文

微服务的力量来自明确定义其职责并划分它们之间的边界。此处的目标是在边界内建设高凝聚力,并在边界外建设低耦合(banq 注:高凝聚低耦合)。也就是说,趋于一起扭转的事物应该属于同一事物。就像在许多现实生活中的问题一样,这说起来容易做起来难,业务在一直倒退,逻辑的假如前提也会随之扭转。因而,重构的能力是设计零碎时要思考的另一关键问题。

畛域驱动设计(DDD)是要害,在咱们看来,这是设计微服务时必不可少的工具,无论是突破整体还是施行未开发我的项目。畛域驱动的设计(Eric Evans 在他的书中提出)是一组思维、原理和模式,可帮忙基于业务畛域的根底模型设计软件系统。开发人员和领域专家独特单干,以通用的通用语言创立业务模型。而后,他们将这些模型绑定到有意义的零碎,并在这些零碎与从事这些服务的团队之间建设合作协定。更重要的是,他们设计了零碎之间的概念轮廓或边界。

微服务设计从这些概念中吸取了灵感,因为所有这些原理都有助于构建能够互相独立变动和倒退的模块化零碎。

在持续进行之前,让咱们疾速理解一下 DDD 的一些根本术语。域驱动设计的残缺概述超出了本博客的范畴。咱们强烈建议任何尝试构建微服务的人举荐 Eric Evans 的书籍。

畛域:代表组织的工作。例如它是批发或电子商务。

子域:组织或组织内的业务部门。域由多个子域组成。

无所不在的语言:这是用于表白模型的语言。在上面的示例中,Item 是一个模型,属于这些子域中每个子域的通用语言。开发人员,产品经理,领域专家和业务涉众都批准应用雷同的语言,并在其工件(代码,产品文档等)中应用该语言。

有界上下文:域驱动的设计将有界上下文定义为“单词或语句能确定其含意的设置”。简而言之,这意味着模型是有意义的边界。在下面的示例中,“我的项目”在每种上下文中的含意不同。在目录上下文中,我的项目示意可售产品,而在购物车上下文中,则示意客户已将其增加到购物车中的我的项目。在“运输”上下文中,它示意将要运送给客户的仓库物料。这些模型中的每一个都是不同的,并且每个都有不同的含意,并且可能蕴含不同的属性。通过将这些模型拆散并隔离在它们各自的边界内,咱们能够自在地表白模型而没有歧义。

留神:必须理解子域和有界上下文之间的区别。子域属于问题空间,即您的企业如何对待问题,而受限上下文属于解决方案空间,即咱们将如何施行问题的解决方案。从实践上讲,每个子域可能具备多个有界上下文,只管咱们致力为每个子域提供一个有界上下文。

微服务与无限上下文如何相干

当初,微服务在哪里适宜?能够说每个有界上下文都映射到微服务吗?是的,咱们将明确为什么。在某些状况下,有界上下文的边界或轮廓可能很大。

思考下面的例子。定价绑定上下文具备三个不同的模型 - 价格,定价我的项目和折扣,每个模型负责目录我的项目的价格,计算我的项目列表的总价格并别离利用折扣。咱们能够创立一个蕴含以上所有模型的零碎,然而它可能会成为一个不合理的大型应用程序。如前所述,每个数据模型都有其不变性和业务规定。随着工夫的流逝,如果咱们不小心的话,零碎可能会变成一个泥泞的大球,边界含糊,职责重叠,甚至可能回到咱们开始的中央—一个整体。

对系统进行建模的另一种办法是将相干模型拆散或分组为独自的微服务。在 DDD 中,这些模型(价格,定价我的项目和折扣)被称为聚合 Aggregates。聚合是组成相干模型的独立模型。您只能通过已公布的界面更改聚合的状态,并且聚合可确保一致性,并且不变量保持良好状态。

聚合是关联对象的集群,被视为数据更改的单元。内部援用仅限于 AGGREGATE 的一个成员,称为根。一组一致性规定实用于 AGGREGATE 的边界。

同样,没有必要肯定要将每个聚合建模为一个独特的微服务。图中的服务(聚合)是一对一关系,但这不肯定是规定。在某些状况下,在单个服务中托管多个聚合可能是有意义的,尤其是当咱们不齐全理解业务畛域时。须要留神的重要一点是,只能在单个聚合中保障一致性,并且只能通过已公布的界面批改聚合。任何违反这些规定的行为都有变成大泥球的危险。

上下文映射—准确划分微服务边界的一种办法

整体构造通常由不同的模型组成,大多数模型是严密耦合的 - 模型可能晓得彼此的密切细节,更改一个模型可能会对另一个模型产生副作用,依此类推。合成整体时,确定这些模型(在这种状况下为汇合)及其关系至关重要。上下文映射能够帮忙咱们做到这一点。它们用于标识和定义各种有界上下文和聚合之间的关系。在下面的示例中,有界上下文定义了模型的边界(价格,折扣等),而上下文映射定义了这些模型之间以及不同有界上下文之间的关系。

上下文映射的残缺摸索不在本博客的探讨范畴之内,但咱们将通过一个示例进行阐明。下图显示了解决电子商务订单付款的各种应用程序。

  1. 购物车上下文负责订单的在线受权;订单上下文流程过帐付款后的付款流程;联系核心会解决所有例外情况,例如重试付款和更改用于订单的付款形式
  2. 为了简略起见,让咱们假如所有这些上下文都是作为独自的服务实现的
  3. 所有这些上下文封装了雷同的模型。
  4. 请留神,这些模型在逻辑上是雷同的。也就是说,它们都遵循雷同的通用域语言 - 付款形式,受权和结算。只是它们是不同上下文的一部分。

从新定义服务边界—将聚合映射到正确的上下文

谬误案例如下图:

电子商务中所有模型都间接与单个领取聚合的网关上下文(payment gateway context)集成,领取须要保障事务性,然而因为与多个服务集成,领取的事务性就不能通过在各种服务之间强制执行不变性和一致性来实现,(banq 注:当然有人就提出 分布式事务 的概念来在这些不同服务之间实现领取过程的事务性,这其实是在谬误设计根底上的伪概念)。

请留神,领取网关中的任何更改都将迫使更改多个服务,并可能更改多个团队,因为不同的组能够领有这些上下文。

进行一些调整并使聚合与正确的上下文对齐,咱们能够更好地示意这些子域如下图。产生了很多变动。让咱们回顾一下更改:

通常,整体 (单体) 或遗留应用程序具备许多聚合,通常具备重叠的边界。创立这些聚合及其依赖关系的上下文地图有助于咱们理解将要从这些整体中解脱进去的任何新微服务的轮廓。请记住,微服务架构的胜利或失败取决于聚合之间的低耦合以及这些聚合之间的高内聚性。

同样重要的是要留神,有界上下文自身就是适合的内聚单元。即便上下文具备多个聚合,整个上下文及其聚合也能够组成一个微服务。咱们发现这种启发式办法对于有些艰涩的畛域特地有用 - 思考组织正在涉足的新业务畛域。您可能对拆散的正确边界没有足够的理解,并且聚集体的任何过早合成都可能导致低廉的重构。设想一下,因为数据迁徙,不得不将两个数据库合并为一个,因为咱们偶尔发现两个聚合属于同一类。然而请确保通过接口将这些聚合充沛隔离,以使它们不晓得彼此的简单细节。

事件风暴 - 辨认服务边界的另一种技术

事件风暴是识别系统中的聚合(以及微服务)的另一种必不可少的技术。这对于毁坏整体构造以及设计简单的微服务生态系统都是有用的工具。咱们曾经应用这种技术合成了一个简单的应用程序,并且打算在独自的博客中介绍 Event Storming 的教训。对于此博客的范畴,咱们想给出一个疾速的高级概述。如果您有趣味进一步摸索,请观看 Alberto Brandelloni 的视频。

简而言之,事件风暴是在应用程序团队(在咱们的状况下为整体)中进行的头脑风暴,以识别系统中产生的各种 畛域事件 和流程。团队还确定这些事件影响及其后续影响的汇总或模型。在团队进行此练习时,他们会确定不同的重叠概念,不置可否的畛域语言以及互相抵触的业务流程。他们将相干模型分组,从新定义聚合并确定反复的过程。随着这些工作的进行,这些汇合所属的无限上下文变得清晰起来。如果所有团队都在同一个房间(物理或虚构)中,并开始在 Scrum 格调的白板上绘制事件,命令和过程的映射,那么 Event Storming 研讨会将十分有用。在本练习完结时,

  1. 从新定义的聚合列表。这些可能成为新的微服务
  2. 这些微服务之间须要流动的域事件
  3. 间接从其余应用程序或用户调用的命令

咱们在一场 Event Storming 研讨会完结时展现了一个示例板。对于团队来说,这是一次很棒的合作流动,以就正确的聚合和无限的上下文达成统一。除了进行杰出的团队建设流动外,团队在本次会议中怀才不遇,对畛域,通用语言和准确的服务边界有着独特的了解。

微服务之间的通信

一个整体在一个流程边界内托管了多个聚合体。因而,在此边界内能够治理聚合体的事务一致性,例如,如果客户下订单,咱们能够缩小我的项目的库存,并向客户发送电子邮件,全副都在一次交易事务中。所有操作都会胜利,或者全副都会失败。然而,当咱们突破整体并将聚合分布到不同的环境中时,咱们将领有数十甚至数百个微服务。迄今为止,在整体构造的单个边界内存在的流程当初散布在多个 分布式系统 中。要在所有这些分布式系统上实现事务的完整性和一致性十分艰难,而且要付出代价 - 零碎的可用性。

微服务也是分布式系统。因而,CAP 定理也实用于它们 -“分布式系统只能提供三个所需特色中的两个:一致性,可用性和分区容限(CAP 中的“C”,“A”和“P”)。”在事实世界的零碎中,分区容忍度是无奈商议的 - 网络不牢靠,虚拟机可能宕机,区域之间的提早会变得更糟,等等。

因而,咱们能够抉择“可用性”或“一致性”。当初,咱们晓得在任何古代应用程序中,就义可用性也不是一个好主见。

围绕最终一致性设计应用程序

如果您尝试跨多个分布式系统构建事务,那么您将再次陷入困境。变成最蹩脚的一种分布式整体事务。如果任何一个零碎点不可用,则整个流程将不可用,通常会导致令人丧气的客户体验、失去保障失败的承诺等。

此外,对一项服务的更改通常可能须要对另一项服务进行更改,从而导致简单而低廉的部署。因而,咱们最好依据本人的用例来设计应用程序,以容忍一点点的不一致性,以进步可用性。对于下面的示例,咱们能够使所有过程 异步,从而最终保持一致。咱们能够异步发送电子邮件,而与其余流程无关。

如果承诺的物品当前在仓库中不可用,该我的项目可能被延期订购,或者咱们能够进行承受超过某个阈值的我的项目的订单。

有时,您可能会遇到一个场景,该场景可能须要逾越不同流程边界的两个聚合中的强 ACID 款式事务。这是从新查看这些聚合并将它们合并为一个极好的标记。在咱们开始在不同过程边界中合成这些聚合之前,事件风暴和上下文映射将有助于及早辨认这些依赖性。将两个微服务合并为一个老本很高,这是咱们应该致力防止的事件。

反对事件驱动的架构

微服务可能会在其聚合上收回实质上的变动。这些称为畛域事件,并且对这些更改感兴趣的任何服务都能够侦听这些事件并在其域内采取相应的操作。这种办法防止了任何行为上的耦合:一个域不规定其余域应该做什么,以及工夫耦合 - 一个过程的胜利实现不依赖于同时可用的所有零碎。当然,这将意味着零碎最终将保持一致。

在下面的示例中,订单服务公布了一个事件 - 订单已勾销。订阅该事件的其余服务解决其各自的域性能:付款服务退还款项,库存服务调整我的项目的库存,依此类推。要确保此集成的可靠性和弹性的几点注意事项:

  • 生产者应确保至多生产一次事件。如果这样做失败,则应确保存在回退机制以从新触发事件
  • 消费者应确保以幂等的形式生产事件。如果再次发生同一事件,那么在用户端不应有任何副作用。事件也可能不按程序达到。消费者能够应用工夫戳记或版本号字段来保障事件的唯一性。

因为某些用例的性质,不肯定总是能够应用基于事件的集成。请查看购物车服务和付款服务之间的集成。这是一个同步集成,因而咱们须要留神一些事项。这是行为耦合的一个示例 -Cart 服务可能从 Payment 服务中调用 REST API,并批示其受权订单付款,而工夫耦合则须要 Payment 服务用于 Cart 服务能力承受订单。这种耦合缩小了这些上下文的自治性,并且可能缩小了不心愿的依赖性。有几种办法能够防止这种耦合,然而应用所有这些选项,咱们将失去向客户提供即时反馈的能力。

  • 将 REST API 转换为基于事件的集成。然而,如果领取服务仅公开 REST API,则此选项可能不可用
  • 购物车服务立刻承受订单,并且有一个批处理作业来接管订单并调用领取服务 API
  • 购物车服务会产生一个本地事件,而后调用付款服务 API

在失败和上游依赖项(付款服务)不可用的状况下,将上述内容与重试联合应用能够使设计更具弹性。例如,在产生故障的状况下,能够通过事件或基于批次的重试来备份购物车和付款服务之间的同步集成。这种办法会对客户体验产生额定的影响:客户可能输出了不正确的付款明细,并且当咱们离线解决付款时,咱们不会将其在线。否则,发出失败的付款可能会减少业务老本。然而,很有可能,购物车服务对于领取服务的不可用性或故障具备弹性,其毛病胜于毛病。例如,如果咱们无奈离线收款,咱们能够告诉客户。

防止针对特定消费者数据需要的服务之间的编排

任何面向服务的体系结构中的反模式之一是,这些服务都能够满足消费者的特定拜访模式。通常,这产生在消费者团队与服务团队严密单干时。如果团队正在开发整体应用程序,则他们通常会创立一个跨不同聚合边界的繁多 API,从而严密耦合这些聚合。

让咱们思考一个例子。说 Web 中的“订单详细信息”页面,挪动应用程序须要在单个页面上同时显示订单的详细信息和针对该订单解决的退款的详细信息。在整体应用程序中,Order GET API(假如它是 REST API)一起查问 Orders 和 Refunds,合并两个聚合,而后将复合响应发送给调用方。因为聚合属于雷同的过程边界,因而无需太多开销即可执行此操作。因而,消费者能够在一个调用中取得所有必要的数据。

如果订单和退款是不同上下文的一部分,则数据不再存在于单个微服务或聚合边界内。为消费者保留雷同性能的一种抉择是使订单服务负责调用退款服务并创立复合响应。此办法引起以下问题:

  1. 订单服务当初与另一个服务集成在一起,纯正是为了反对须要退款数据和订单数据的消费者。当初,订单服务的自治性升高了,因为退款总额中的任何更改都将导致订单总额中的更改。
  2. 订单服务具备另一个集成,因而要思考另一个故障点 - 如果退款服务呈现故障,订购服务是否仍能够发送局部数据,并且消费者能够失常地故障吗?
  3. 如果消费者须要更改以从“退款”聚合中获取更多数据,则当初须要两个团队来进行更改
  4. 如果在整个平台上都遵循这种模式,则可能导致各种域服务之间的依存关系盘根错节,所有这些都是因为这些服务满足了调用者的特定拜访模式。

前端的后端 BFF

加重这种危险的一种办法是让消费者团队治理各种域服务之间的编排。毕竟,呼叫者会更好地理解拜访模式,并且能够齐全管制对这些模式的任何更改。这种办法将域服务与表示层拆散开来,使它们专一于外围业务流程。然而,如果 Web 和挪动应用程序开始间接调用不同的服务而不是从整体中调用一个复合 API,则可能会导致这些应用程序的性能开销–通过较低带宽网络进行屡次调用,解决和合并来自不同 API 的数据,等等。。

相同,能够应用另一种称为前端的后端的模式。在这种 设计模式 下,由消费者(在本例中为 Web 和挪动团队)创立和治理的后端服务负责跨多个域服务的集成,纯正是为了向客户提供前端体验。Web 和挪动团队当初能够依据他们的用例设计数据合同。他们甚至能够应用 GraphQL 而不是 REST API 来灵便地查问并精确获取所需的信息。

重要的是要留神,此服务是由使用者团队领有和保护的,而不是由领有域服务的团队领有和保护的。前端团队当初能够依据本人的需要进行优化 - 挪动应用程序能够申请更小的无效负载,缩小来自挪动应用程序的呼叫次数等等。查看上面的业务流程的订正视图。

论断

在此博客中,咱们涉及了各种概念,策略和设计启发法,以便在咱们进入微服务畛域时,尤其是在尝试将整体式服务拆分为基于域的微服务时,加以思考。其中许多主题自身就是广大的主题,我认为咱们没有做出足够的公正性来具体解释它们,然而咱们想介绍一些要害主题以及咱们采纳这些主题的教训。进一步浏览(链接)局部提供了一些参考资料和一些有用的内容,供任何心愿采纳此办法的人应用。

原文:https://medium.com/walmartglo… \
译文:jdon.com/54558

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0