This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -550,7 +550,7 @@ function hide_canvas() {
<p>这两种级别的代码模型仅仅存在编译期的差异,后者的解耦会更加彻底,倘若限界上下文的划分足够合理,也能提高它们对变化的应对能力。例如,当限界上下文 A 的业务场景发生变更时,我们可以只修改和重编译限界上下文 A 对应的 Jar 包,其余 Jar 包并不会受到影响。由于它们都运行在同一个 Java 虚拟机中,意味着当变化发生时,整个系统需要重新启动和运行。</p>
<p><strong>即使处于同一个进程的边界,我们仍需重视代码模型的边界划分</strong>,因为这种边界隔离有助于整个系统代码结构变得更加清晰。限界上下文之间若采用进程内通信,则彼此之间的协作会更加容易、更加高效。然而,正所谓<strong>越容易重用,就越容易产生耦合</strong>。编写代码时我们需要谨守这条无形的逻辑边界时刻注意不要逾界并确定限界上下文各自对外公开的接口避免它们之间产生过多的依赖。此时防腐层ACL就成了抵御外部限界上下文变化的最佳场所。一旦系统架构需要将限界上下文调整为进程间的通信边界这种“各自为政”的设计与实现能够更好地适应这种演进。</p>
<p>以第 10 课介绍的项目管理系统为例,假设项目上下文与通知上下文之间的通信为进程内通信,当项目负责人将 Sprint Backlog 成功分配给团队成员之后,系统将发送邮件通知该团队成员。这个职责由项目上下文的 AssignSprintBacklogService 领域服务承担,而发送通知的职责则由通知上下文的 NotificationAppService 应用服务承担。考虑到未来限界上下文通信边界的变化,我们就不能直接在 AssignSprintBacklogService 服务中实例化 NotificationAppService 对象,而是在项目上下文中定义通知服务的接口 NotificationService并由 NotificationClient 去实现这个接口它们扮演的就是防腐层的作用。AssignSprintBacklogService 服务依赖该防腐层的接口,并将具体实现通过依赖注入。这个协作过程如下面的时序图所示:</p>
<p><img src="assets/66535a00-b669-11e8-825a-a31adc0db7e6" alt="enter image description here" /></p>
<p><img src="assets/66535a00-b669-11e8-825a-a31adc0db7e6" alt="png" /></p>
<p>倘若在未来需要将通知上下文分离为进程间的通信边界,这种变动将只会影响到防腐层的实现,作为 NotificationService 服务的调用者,并不会受到这一变化的影响。</p>
<p>采用进程内通信的系统架构属于单体Monolithic架构所有限界上下文部署在同一个进程中因此不能针对某一个限界上下文进行水平伸缩。当我们需要对限界上下文的实现进行替换或升级时也会影响到整个系统。即使我们守住了代码模型的边界耦合仍然存在导致各个限界上下文的开发互相影响团队之间的协调成本也随之而增加。</p>
<h4>进程间的通信边界</h4>
@@ -570,7 +570,7 @@ function hide_canvas() {
<p>根据 Netflix 团队提出的微服务架构最佳实践,其中一个最重要特征就是“<strong>每个微服务的数据单独存储</strong>但是服务的分离并不绝对代表数据应该分离。数据库的样式Schema与领域模型未必存在一对一的映射关系。在对数据进行分库设计时如果仅仅站在业务边界的角度去思考可能会因为分库的粒度太小导致不必要的跨库关联。因此我们可以将“数据库共享”模式视为一种过渡方案。如果没有想清楚微服务的边界就不要在一开始设计微服务时就直接将数据彻底分开而应采用演进式的设计。</p>
<p>为了便于在演进设计中将分表重构为分库,从一开始要<strong>注意避免在分属两个限界上下文的表之间建立外键约束关系</strong>。某些关系型数据库可能通过这种约束关系提供级联更新与删除的功能,这种功能反过来会影响代码的实现。一旦因为分库而去掉表之间的外键约束关系,需要修改的代码太多,会导致演进的成本太高,甚至可能因为某种疏漏带来隐藏的 Bug。</p>
<p>如果设计数据表时没有外键约束关系,可能在当前增加了开发成本,却为未来的演进打开了方便之门。例如,在针对某手机品牌开发的舆情分析系统中,危机查询服务提供对识别出来的危机进行查询。查询时,需要通过 userID 获得危机处理人、危机汇报人的详细信息。左图为演进前直接通过数据库查询的方式,右图则切断了这种数据库耦合,改为服务调用的方式:</p>
<p><img src="assets/77a7ff40-b669-11e8-a3e4-bd0fed6937a6" alt="enter image description here" /></p>
<p><img src="assets/77a7ff40-b669-11e8-a3e4-bd0fed6937a6" alt="png" /></p>
<p>数据库共享架构也可能是一种“反模式”。当两个分处不同限界上下文的服务需要操作同一张数据表(这张表被称之为“共享表”)时,就传递了一个信号,即我们的设计可能出现了错误。</p>
<ul>
<li>遗漏了一个限界上下文,共享表对应的是一个被重用的服务:买家在查询商品时,商品服务会查询价格表中的当前价格,而在提交订单时,订单服务也会查询价格表中的价格,计算当前的订单总额;共享价格数据的原因是我们遗漏了价格上下文,通过引入价格服务就可以解除这种不必要的数据共享。</li>
@@ -580,7 +580,7 @@ function hide_canvas() {
<p>为什么会出现这三种错误的设计?<strong>根本原因在于我们没有通过业务建模,而是在数据库层面隐式地进行建模</strong>,因而在代码中没有体现正确的领域模型,从而导致了数据库的耦合或共享。遵循领域驱动设计的原则,我们应该根据领域逻辑去识别限界上下文。</p>
<h5><strong>零共享架构</strong></h5>
<p>当我们将两个限界上下文共享的外部资源彻底斩断后,就成为了<strong>零共享架构</strong>。例如,前面介绍的舆情分析系统,在去掉危机查询对用户表的依赖后,就演进为零共享架构。如下图所示,危机分析上下文与用户上下文完全零共享:</p>
<p><img src="assets/87aad020-b669-11e8-a11e-1594b1f38679" alt="enter image description here" /></p>
<p><img src="assets/87aad020-b669-11e8-a11e-1594b1f38679" alt="png" /></p>
<p>这是一种限界上下文彻底独立的架构风格,它保证了边界内的服务、基础设施乃至于存储资源、中间件等其他外部资源的完整性与独立性,最终形成自治的微服务。这种架构的表现形式为:每个限界上下文都有自己的代码库、数据存储以及开发团队,每个限界上下文选择的技术栈和语言平台也可以不同,限界上下文之间仅仅通过限定的通信协议和数据格式进行通信。</p>
<p>此时的限界上下文形成了一个相对自由的“独立王国”。从北向网关的 Controller 到应用层,从应用层到领域层的领域模型,再到南向网关对数据库的访问实现,进而到数据库的选型都可以由当前限界上下文独立做主。由于它们是“零共享”的,使得它们彼此之间可以独立演化,在技术选型上也可以结合自己上下文的业务场景做出“恰如其分”的选择。譬如说,危机分析需要存储大规模的非结构化数据,同时业务需要支持对危机数据的全文本搜索,我们选择了 ElasticSearch 作为持久化的数据库。考虑到开发的高效以及对 JSON 数据的支持,我们选择了 Node.js 为后端开发框架。至于用户上下文,数据量小,结构规范,采用传统的基于关系型数据库的架构会更简单更适合。二者之间唯一的耦合就是危机分析通过 HTTP 协议访问上游的用户服务,根据传入的 userID 获得用户的详细信息。</p>
<p>彻底分离的限界上下文变得小而专,使得我们可以很好地安排遵循 2PTs 规则的小团队去治理它。然而,这种架构的复杂度也不可低估。限界上下文之间的通信是跨进程的,我们需要考虑通信的健壮性。数据库是完全分离的,当需要关联之间的数据时,需得跨限界上下文去访问,无法享受数据库自身提供的关联福利。由于每个限界上下文都是分布式的,如何保证数据的一致性也是一件棘手的问题。当整个系统都被分解成一个个可以独立部署的限界上下文时,运维与监控的复杂度也随之而剧增。</p>