This commit is contained in:
周伟
2022-05-11 18:46:27 +08:00
commit 387f48277a
8634 changed files with 2579564 additions and 0 deletions

View File

@@ -0,0 +1,970 @@
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>15 如何设计支持微服务的技术中台?.md</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/DDD 微服务落地实战/00 开篇词 让我们把 DDD 的思想真正落地.md">00 开篇词 让我们把 DDD 的思想真正落地.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/01 DDD :杜绝软件退化的利器.md">01 DDD :杜绝软件退化的利器.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/02 以电商支付功能为例演练 DDD.md">02 以电商支付功能为例演练 DDD.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/03 DDD 是如何落地到数据库设计的?.md">03 DDD 是如何落地到数据库设计的?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/04 领域模型是如何指导程序设计的?.md">04 领域模型是如何指导程序设计的?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/05 聚合、仓库与工厂:傻傻分不清楚.md">05 聚合、仓库与工厂:傻傻分不清楚.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/06 限界上下文:冲破微服务设计困局的利器.md">06 限界上下文:冲破微服务设计困局的利器.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/07 在线订餐场景中是如何开事件风暴会议的?.md">07 在线订餐场景中是如何开事件风暴会议的?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/08 DDD 是如何解决微服务拆分难题的?.md">08 DDD 是如何解决微服务拆分难题的?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/09 DDD 是如何落地微服务设计实现的?.md">09 DDD 是如何落地微服务设计实现的?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/10 微服务落地的技术实践.md">10 微服务落地的技术实践.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/11 解决技术改造困局的钥匙:整洁架构.md">11 解决技术改造困局的钥匙:整洁架构.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/12 如何设计支持快速交付的技术中台战略?.md">12 如何设计支持快速交付的技术中台战略?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/13 如何实现支持快速交付的技术中台设计?.md">13 如何实现支持快速交付的技术中台设计?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/14 如何设计支持 DDD 的技术中台?.md">14 如何设计支持 DDD 的技术中台?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/DDD 微服务落地实战/15 如何设计支持微服务的技术中台?.md">15 如何设计支持微服务的技术中台?.md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计).md">16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计).md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/17 基于 DDD 的微服务设计演示(含支持微服务的 DDD 技术中台设计).md">17 基于 DDD 的微服务设计演示(含支持微服务的 DDD 技术中台设计).md.html</a>
</li>
<li>
<a href="/专栏/DDD 微服务落地实战/18 基于事件溯源的设计开发.md">18 基于事件溯源的设计开发.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>15 如何设计支持微服务的技术中台?</h1>
<p>有了技术中台对领域驱动的支持,那如何应用于微服务架构呢?支持 DDD 与微服务的技术中台应当具备以下几个方面的能力:</p>
<ul>
<li><strong>解决当前微服务架构的技术不确定性</strong>,使得微服务项目可以用更低的成本应对日后技术架构的更迭;</li>
<li>可以更加容易地将<strong>领域驱动设计应用到微服务架构中</strong>,包括:领域建模、限界上下文的微服务拆分、事件通知机制等;</li>
<li>需要解决领域对象在仓库与工厂进行装配时,如何将本地查询替换为远程接口调用,实现微服务间数据装配的问题。</li>
</ul>
<h3>解决技术不确定性的问题</h3>
<p>如今的微服务架构,基本已经形成了 Spring Cloud 一统天下的局势。然而,在 Spring Cloud 框架下的各种技术组件依然存在<strong>诸多不确定性</strong>,如:注册中心是否采用 Eureka、服务网关是采用 Zuul 还是 Gateway等等。同时服务网格 Service Mesh 方兴未艾,不排除今后所有的微服务都要切换到 Service Mesh 的可能。在这种情况下如何<strong>决策微服务的技术架构</strong>?代码尽量不要与 Spring Cloud 耦合,才能在将来更容易地切换到 Service Mesh。那么具体又该如何做到呢</p>
<p><img src="assets/Cip5yF_q1GWABFvEAAK9qvAoHxc276.png" alt="Drawing 0.png" /></p>
<p>单 Controller、单 Dao 的设计在微服务架构的应用</p>
<p>如上图所示,当前端通过服务网关访问微服务时,首先要<strong>访问聚合层的微服务</strong>。这时,在聚合层的微服务中,采用单 Controller 接收前端请求。这样,只有该 Controller 与 MVC 框架耦合,后面所有的 Service 不会耦合,从而实现了<strong>业务代码与技术框架的分离</strong></p>
<p>同样的,当 Service 执行各种操作调用原子服务层的微服务时,不是通过 Ribbon 进行远程调用,而是将原子服务层的微服务开放的接口,在聚合层微服务的本地编写一个 Feign 接口。那么,聚合层微服务在调用原子微服务时,实际调用的是<strong>自己本地的接口</strong>,再由这个接口通过加载 Feign 注解,去实现远程调用。</p>
<p>这样,聚合层微服务中的各个 Service 就不会与 Spring Cloud 各个组件发生任何耦合,只有那些 Feign 接口与 Spring Cloud 耦合去实现远程调用,让业务代码与技术框架实现了解耦。</p>
<p>同样的道理,原子服务层的微服务在对外开放接口时,不是由各个 Service 直接开放 API 接口。因为如果让 Service 直接开放 API 接口,就需要编写相关注解,使得 Service 与 Spring Cloud 耦合。因此,由统一的 Controller 开放接口,再由它去调用内部的 Service。这样所有的 Service 就是纯洁的,不与 Spring Cloud 技术框架耦合,只有 Controller 与其耦合。</p>
<p>有了以上这些设计,当未来需要从 Spring Cloud 框架迁移到 Service Mesh 时,只需要将那些纯洁的、不与 Spring Cloud 耦合的 Service与领域对象中的业务代码迁移到新的框架中就可以以非常低的成本在各种技术框架中自由地切换从而快速跟上技术发展的步伐。通过这种方式就能很好地应对未来的技术不确定性问题更好地开展架构演化。</p>
<h3>支持微服务远程调用的架构设计</h3>
<p>此外,微服务架构设计最大的难题是<strong>微服务的合理拆分</strong>,拆分要体现单一职责原则,即微服务间低耦合,微服务内高内聚。那么,在软件项目中如何做到这些呢?业界最佳的实践无疑是基于领域的设计,即先按照领域业务建模,然后基于限界上下文进行微服务拆分。这样设计出来的微服务系统,都可以保证每次变更落到某个微服务上。微服务变更完了,自己独立升级,就能达到降低维护成本、快速交付的目的。</p>
<p>基于这样的思路,每个微服务在设计时,都采用支持领域驱动的技术中台。这样,每个微服务都是基于领域驱动建模和设计,然后在该技术中台中编码实现,既提高了开发速度,又降低了维护成本。</p>
<p>然而,转型为微服务后,有一个技术难题亟待解决,那就是<strong>跨库的数据操作</strong>。当一个单体应用拆分成多个微服务后,不仅应用程序需要拆分,数据库也需要拆分。譬如,经过微服务拆分,订单有订单数据库,用户有用户数据库。这时,当查询订单,需要补填其对应的用户信息时,就不能从自己本地的数据库中查询了,而是调用“用户”微服务的远程接口,在用户数据库中查询,然后返回给“订单”微服务。这时,原有的技术中台就必须做出调整。</p>
<p>如何调整呢?<strong>通用 DDD 仓库</strong>在执行查询或者装载操作时,查询完订单补填用户信息,不是通过 DAO 去查询本地数据库,而是改成调用远程接口,去调用用户微服务。这时,可以先在订单微服务的本地编写一个用户 Service 的 Feign 接口,订单仓库与工厂调用这个接口就可以了。然后通过 Feign 接口实现对用户微服务的远程调用。</p>
<h3>采用 Feign 接口实现远程调用</h3>
<p>每个微服务都是一个独立的进程,运行在各自独立的 JVM甚至不同的物理节点上通过网络访问。因此微服务与微服务之间的调用必然是<strong>远程调用</strong>。以往,我们对微服务间的调用采用 Ribbon 的方式,在程序中的任意一个位置,只要注入一个 restTemplate就可以进行远程调用。</p>
<p>这样的代码过于随意,会越来越难于阅读与变更维护。比如,原来某个微服务中有两个模块 A 与 B都需要调用模块 C。随着业务变得越来越复杂需要进行微服务拆分将模块 C 拆分到了另外一个微服务中。这时,原来的模块 A 与 B 就不能像原来一样调用模块 C否则就会报错。</p>
<p><img src="assets/CgpVE1_q1HuAOBfeAAC9Nd34EOU948.png" alt="Drawing 1.png" /></p>
<p>Ribbon 的远程调用方式</p>
<p>如何解决以上问题呢?需要同时改造模块 A 与 B分别加入 restTemplate 实现远程调用,来调用模块 C。也就是说这时所有调用模块 C 的程序都需要改造,改造的成本与风险就会比较高。</p>
<p>因此在实现微服务间调用时我们通常会采用另外一个方案Feign。Feign 不是另起炉灶,而是对 Ribbon 的封装,目的是使代码更加规范、变更更加易于维护。采用的方案是,不修改模块 A 与 B 的任何代码,而是在该微服务的本地再制作一个模块 C 的接口 C。该接口与模块 C 一模一样,拥有模块 C 的所有方法,因此模块 A 与 B 还可以像以前一样在本地调用接口 C。但接口 C 只是一个接口,什么都做不了,因此需要通过添加 Feign 的注解,实现远程调用,去调用模块 C。这个方案既没有修改模块 A 与 B又没有修改模块 C而仅仅添加了一个接口 C使维护成本降到了最低。</p>
<p><img src="assets/Cip5yF_q1IOAXztgAAC6-Yd2p84518.png" alt="Drawing 2.png" /></p>
<p>Feign 的远程调用方式</p>
<p>如何通过 Feign 实现微服务的远程调用呢?</p>
<p>首先,创建项目时,在 POM.xml 文件中添加 Eureka Client、Hystrix 与 Actuator 等组件以外,将 ribbon 改为 feign</p>
<pre><code> &lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- 断路器 hystrix --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-netflix-hystrix&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- 断路器监控 --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>接着,在启动类 FeignApplication 中,不仅添加 Discovery Client还要添加 Feign Client</p>
<pre><code>import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author fangang
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
public class FeignApplication {
/**
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(FeignApplication.class, args);
}
}
</code></pre>
<p>用 Feign 实现调用时,首先在消费者这端编写一个与生产者开放的 API 一模一样的接口,然后添加 Feign 的注解:</p>
<pre><code>/**
* The service of suppliers.
* @author fangang
*/
@FeignClient(value=&quot;service-supplier&quot;, fallback=SupplierHystrixImpl.class)
public interface SupplierService {
/**
* @param id
* @return the supplier
*/
@RequestMapping(value = &quot;orm/supplier/loadSupplier&quot;, method = RequestMethod.GET)
public Supplier loadSupplier(@RequestParam(&quot;id&quot;)Long id);
/**
* @param ids
* @return the list of suppliers
*/
@PostMapping(&quot;orm/supplier/loadSuppliers&quot;)
public List&lt;Supplier&gt; loadSuppliers(@RequestParam(&quot;ids&quot;)List&lt;Long&gt; ids);
/**
* @return the list of suppliers
*/
@GetMapping(&quot;orm/supplier/listOfSuppliers&quot;)
public List&lt;Supplier&gt; listOfSuppliers();
}
</code></pre>
<p>在这一代码例子中,具体的流程是这样的:</p>
<ul>
<li>在生产者那一端有个 SupplierService 的类因此首先在消费者这端编写一个一模一样的SupplierService 的接口;</li>
<li>接着,在 interface 前面添加注解 @FeignClient</li>
<li>这里的 value 为生产者在 Eureka 注册中心中注册的名称;</li>
<li>在每个方法前添加注解GET 请求就写 @GetMappingPOST 请求就写 @PostMapping名称就是生产者那端开放的接口名称</li>
<li>然后,如果需要将参数加入 url 中,就在参数前添加注解 @RequestParam</li>
<li>有了以上注解Feign 就可以从接口中取出相应的数据,拼装成 url最后去执行 ribbon 实现微服务远程调用。</li>
</ul>
<h3>采用 Ref 标签调用 Feign 接口</h3>
<p>采用 Feign 实现微服务间的远程调用以后,在 vObj.xml 中进行建模时也需要做出改变将join 标签改为 ref 标签。其配置如下所示:</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;vobjs&gt;
&lt;vo class=&quot;com.demo2.product.entity.Product&quot; tableName=&quot;Product&quot;&gt;
&lt;property name=&quot;id&quot; column=&quot;id&quot; isPrimaryKey=&quot;true&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;name&quot; column=&quot;name&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;price&quot; column=&quot;price&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;unit&quot; column=&quot;unit&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;classify&quot; column=&quot;classify&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;supplier_id&quot; column=&quot;supplier_id&quot;&gt;&lt;/property&gt;
&lt;ref name=&quot;supplier&quot; refKey=&quot;supplier_id&quot; refType=&quot;manyToOne&quot; bean=&quot;com.demo2.product.service.SupplierService&quot; method=&quot;loadSupplier&quot; listMethod=&quot;loadSuppliers&quot;&gt;&lt;/ref&gt;
&lt;/vo&gt;
&lt;vo class=&quot;com.demo2.product.entity.Supplier&quot; tableName=&quot;Supplier&quot;&gt;
&lt;property name=&quot;id&quot; column=&quot;id&quot; isPrimaryKey=&quot;true&quot;&gt;&lt;/property&gt;
&lt;property name=&quot;name&quot; column=&quot;name&quot;&gt;&lt;/property&gt;
&lt;/vo&gt;
&lt;/vobjs&gt;
</code></pre>
<p>在这一配置中,将 supplier 由 join 标签改为 ref 标签,其中:</p>
<ul>
<li><strong>bean</strong>代表在“产品”微服务本地调用“供应商”微服务的 Feign 接口;</li>
<li><strong>method</strong>是指定要调用这个 Feign 接口的方法;</li>
<li><strong>listMethod</strong>是在批量查询“产品”数据集时,进行批量补填的优化措施。</li>
</ul>
<p>通过这样的配置,在查询产品过程中,通用仓库补填供应商信息,就不会去调用本地的 DAO而是调用 SupplierService 的 Feign 接口,由它实现对“供应商”微服务的远程调用,从而实现跨微服务的数据装配。</p>
<h3>总结</h3>
<p>本讲提出了支持 DDD + 微服务架构的技术中台设计思想。通过以上的设计,既实现了业务代码与技术框架解耦的整洁架构思想,使得系统在日后更易于架构演化,又实现了领域模型在微服务间的数据装配,解决了 DDD 转型微服务架构的关键技术难题。开发团队有了这样的技术中台,就能将 DDD 运用起来,在项目中真正的落地实践。</p>
<p>下一讲将在 GitHub 上分享代码,进一步讲解这个技术中台的代码设计与项目实践。</p>
<p><a href="https://github.com/mooodo/demo-service2-support">点击 GitHub 链接</a>,查看源码。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/DDD 微服务落地实战/14 如何设计支持 DDD 的技术中台?.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/DDD 微服务落地实战/16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计).md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70996ecbbb423d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>