learn.lianglianglee.com/专栏/SpringCloud微服务实战(完)/07 如何调用本业务模块外的服务——服务调用.md.html
2022-05-11 18:57:05 +08:00

1003 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>07 如何调用本业务模块外的服务——服务调用.md.html</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="/专栏/SpringCloud微服务实战/00 开篇导读.md.html">00 开篇导读.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/01 以真实“商场停车”业务切入——需求分析.md.html">01 以真实“商场停车”业务切入——需求分析.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/02 具象业务需求再抽象分解——系统设计.md.html">02 具象业务需求再抽象分解——系统设计.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/03 第一个 Spring Boot 子服务——会员服务.md.html">03 第一个 Spring Boot 子服务——会员服务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/04 如何维护接口文档供外部调用——在线接口文档管理.md.html">04 如何维护接口文档供外部调用——在线接口文档管理.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html">05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/06 服务多不易管理如何破——服务注册与发现.md.html">06 服务多不易管理如何破——服务注册与发现.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/SpringCloud微服务实战/07 如何调用本业务模块外的服务——服务调用.md.html">07 如何调用本业务模块外的服务——服务调用.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html">08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/09 热更新一样更新服务的参数配置——分布式配置中心.md.html">09 热更新一样更新服务的参数配置——分布式配置中心.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/10 如何高效读取计费规则等热数据——分布式缓存.md.html">10 如何高效读取计费规则等热数据——分布式缓存.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html">11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/12 同一套服务如何应对不同终端的需求——服务适配.md.html">12 同一套服务如何应对不同终端的需求——服务适配.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html">13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html">14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html">15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/16 服务压力大系统响应慢如何破——网关流量控制.md.html">16 服务压力大系统响应慢如何破——网关流量控制.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/17 集成网关后怎么做安全验证——统一鉴权.md.html">17 集成网关后怎么做安全验证——统一鉴权.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/18 多模块下的接口 API 如何统一管理——聚合 API.md.html">18 多模块下的接口 API 如何统一管理——聚合 API.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/19 数据分库后如何确保数据完整性——分布式事务.md.html">19 数据分库后如何确保数据完整性——分布式事务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/20 优惠券如何避免超兑——引入分布式锁.md.html">20 优惠券如何避免超兑——引入分布式锁.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/21 如何查看各服务的健康状况——系统应用监控.md.html">21 如何查看各服务的健康状况——系统应用监控.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/22 如何确定一次完整的请求过程——服务链路跟踪.md.html">22 如何确定一次完整的请求过程——服务链路跟踪.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/23 结束语.md.html">23 结束语.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>07 如何调用本业务模块外的服务——服务调用</h1>
<p>上篇已经引入 Nacos 基础组件,完成了服务注册与发现机制,可以将所有服务统一的管理配置起来,方便服务间调用。本篇将结合需求点,进行服务间调用,完成功能开发。</p>
<h3>几种服务调用方式</h3>
<p>服务间调用常见的两种方式RPC 与 HTTPRPC 全称 Remote Produce Call 远程过程调用,速度快,效率高,早期的 WebService 接口,现在热门的 Dubbo、gRPC 、Thrift、Motan 等,都是 RPC 的典型代表,有兴趣的小伙伴可以查找相关的资料,深入了解下。</p>
<p>HTTP 协议HyperText Transfer Protocol超文本传输协议是因特网上应用最为广泛的一种网络传输协议所有的 WWW 文件都必须遵守这个标准。对服务的提供者和调用方没有任何语言限定,更符合微服务语言无关的理念。时下热门的 RESTful 形式的开发方式,也是通过 HTTP 协议来实现的。</p>
<p>本次案例更多的考虑到简捷性以及 Spring Cloud 的基础特性,决定采用 HTTP 的形式进行接口交互完成服务间的调用工作。Spring Cloud 体系下常用的调用方式有RestTemplate 、 Ribbon 和 Feign 这三种。</p>
<p><strong>RestTemplate</strong>,是 Spring 提供的用于访问 Rest 服务的客户端RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。</p>
<p><strong>Ribbon</strong>,由 Netflix 出品,更为人熟知的作用是客户端的 Load Balance负载均衡</p>
<p><strong>Feign</strong>,同样由 Netflix 出品,是一个更加方便的 HTTP 客户端,用起来就像调用本地方法,完全感觉不到是调用的远程方法。内部也使用了 Ribbon 来做负载均衡功能。</p>
<p>由于 Ribbon 已经融合在 Feign 中,下面就只介绍 RestTemplate 和 Feign 的使用方法。</p>
<h3>RestTemplate 的应用</h3>
<p>功能需求:会员绑定手机号时,同时给其增加相应的积分。会员绑定手机号在会员服务中完成,增加会员积分在积分服务中完成。请求路径是客户端-&gt;会员服务-&gt;积分服务。</p>
<p>响应客户端请求的方法</p>
<pre><code class="language-java"> @RequestMapping(value = &quot;/bindMobileUseRestTemplate&quot;, method = RequestMethod.POST)
public CommonResult&lt;Integer&gt; bindMobileUseRestTemplate(String json) throws BusinessException{
CommonResult&lt;Integer&gt; result = new CommonResult&lt;&gt;();
log.info(&quot;bind mobile param = &quot; + json);
int rtn = memberService.bindMobileUseRestTemplate(json);
result.setRespData(rtn);
return result;
}
</code></pre>
<p>做好 RestTemplate 的配置工作,否则无法正常使用。</p>
<pre><code class="language-java">@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory simpleClientHttpRequestFactory){
return new RestTemplate(simpleClientHttpRequestFactory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
return factory;
}
}
</code></pre>
<p>在 MemberService 中处理请求,逻辑如下:</p>
<pre><code class="language-java">@Autowired
RestTemplate restTemplate;
@Override
public int bindMobileUseRestTemplate(String json) throws BusinessException {
Member member = JSONObject.parseObject(json, Member.class);
int rtn = memberMapper.insertSelective(member);
// invoke another service
if (rtn &gt; 0) {
MemberCard card = new MemberCard();
card.setMemberId(member.getId());
card.setCurQty(&quot;50&quot;);
MultiValueMap&lt;String, String&gt; requestMap = new LinkedMultiValueMap&lt;String, String&gt;();
requestMap.add(&quot;json&quot;, JSONObject.toJSONString(card).toString());
HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity = new HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt;(
requestMap, null);
String jsonResult = restTemplate.postForObject(&quot;http://localhost:10461/card/addCard&quot;, requestEntity,
String.class);
log.info(&quot;creata member card suc!&quot; + jsonResult);
}
return rtn;
}
</code></pre>
<p>采用 postForObject 形式请求积分服务中的生成积分记录的方法,并传递相应参数。积分子服务中方法比较简单,接受调用请求的方法:</p>
<pre><code class="language-java">@RequestMapping(&quot;card&quot;)
@RestController
@Slf4j
public class MemberCardController {
@Autowired
MemberCardService cardService;
@RequestMapping(value = &quot;/addCard&quot;, method = RequestMethod.POST)
public CommonResult&lt;Integer&gt; addCard(String json) throws BusinessException {
log.info(&quot;eclise service example: begin add member card = &quot; + json);
//log.info(&quot;jar service example: begin add member card = &quot; + json);
CommonResult&lt;Integer&gt; result = new CommonResult&lt;&gt;();
int rtn = cardService.addMemberCard(json);
result.setRespData(rtn);
return result;
}
}
</code></pre>
<p>实际业务逻辑处理部分由 MemberCardService 接口中完成。</p>
<pre><code class="language-java">@Service
@Slf4j
public class MemberCardServiceImpl implements MemberCardService {
@Autowired
MemberCardMapper cardMapper;
@Override
public int addMemberCard(String json) throws BusinessException {
MemberCard card = JSONObject.parseObject(json,MemberCard.class);
log.info(&quot;add member card &quot; +json);
return cardMapper.insertSelective(card);
}
}
</code></pre>
<p>分别启动会员服务、积分服务两个项目,通过 Swagger 接口 UI 作一个简单测试。</p>
<p><img src="assets/2020-05-05-021612.jpg" alt="img" /></p>
<p><img src="assets/2020-05-05-021615.jpg" alt="img" /></p>
<p>RestTemplate 默认依赖 JDK 提供 HTTP 连接的能力,针对 HTTP 请求,提供了不同的方法可供使用,相对于原生的 HTTP 请求是一个进步,但经过上面的代码使用,发现还是不够优雅。能不能像调用本地接口一样,调用第三方的服务呢?下面引入 Feign 的应用,绝对让你喜欢上 Feign 的调用方式。</p>
<h3>Feign 的应用</h3>
<p>Fegin 的调用最大的便利之处在于,屏蔽底层的连接逻辑,让你可以像调用本地接口一样调用第三方服务,代码量更少更优雅。当然,必须在服务注册中心的协调下才能正常完成服务调用,而 RestTemplate 并不关心服务注册心是否正常运行。</p>
<h4>引入 Feign</h4>
<p>Feign 是由 Netflix 开发出来的另外一种实现负载均衡的开源框架,它封装了 Ribbon 和 RestTemplate实现了 WebService 的面向接口编程,进一步的减低了项目的耦合度,因为它封装了 Riboon 和 RestTemplate ,所以它具有这两种框架的功能。 在会员模块的 pom.xml 中添加 jar 引用</p>
<pre><code class="language-xml">&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;
</code></pre>
<p>在模块启动类中增加 <a href="">@EnableFeignClients </a> 注解,才能正常使用 Feign 相关功能,启动时开启对 FeignClient 注解的包扫描,并且扫描到相关的接口客户端。</p>
<pre><code class="language-java">//#client 目录为 feign 接口所在目录
@EnableFeignClients(basePackages = &quot;com.mall.parking.member.client&quot;)
</code></pre>
<p>前端请求响应方法</p>
<pre><code class="language-java"> @RequestMapping(value = &quot;/bindMobile&quot;, method = RequestMethod.POST)
public CommonResult&lt;Integer&gt; bindMobile(String json) throws BusinessException{
CommonResult&lt;Integer&gt; result = new CommonResult&lt;&gt;();
log.info(&quot;bind mobile param = &quot; + json);
int rtn = memberService.bindMobile(json);
result.setRespData(rtn);
return result;
}
</code></pre>
<h4>接口编写</h4>
<p>编写 MemberCardClient与积分服务调用时使用其中的接口与积分服务中的相关方法实行一对一绑定。</p>
<pre><code class="language-java">@FeignClient(value = &quot;card-service&quot;)
public interface MemberCardClient {
@RequestMapping(value = &quot;/card/addCard&quot;, method = RequestMethod.POST)
public CommonResult&lt;Integer&gt; addCard(@RequestParam(value = &quot;json&quot;) String json) throws BusinessException;
@RequestMapping(value = &quot;/card/updateCard&quot;, method = RequestMethod.POST)
public CommonResult&lt;Integer&gt; updateCard(@RequestParam(value = &quot;json&quot;) String json) throws BusinessException;
}
</code></pre>
<blockquote>
<p>注意是 <strong>RequestParam</strong> 不是 <strong>PathVariable</strong> <strong>PathVariable</strong> 是从路径中取值,<strong>RequestParam</strong> 是从参数中取值,用法不一。</p>
</blockquote>
<p>使用时,直接 <a href="">@Autowired </a> 像采用本地接口一样使用即可,至此完成代码的编写,下面再来验证逻辑的准确性。</p>
<ol>
<li>保证 nacos-server 启动中</li>
<li>分别启动 parking-memberparking-card 子服务</li>
<li>通过 parking-member 的 swagger-ui 界面,调用会员绑定手机号接口(或采用 PostMan 工具)</li>
</ol>
<p>正常情况下park-memberpark-card 两个库中数据表均有数据生成。</p>
<p><img src="assets/2020-05-05-021617.jpg" alt="img" /></p>
<p><img src="assets/2020-05-05-021620.jpg" alt="img" /></p>
<p>那么fallback 何时起作用呢?很好验证,当积分服务关闭后,再重新调用积分服务中的生成积分方法,会发现直接调用的是 MemberCardServiceFallback 中的方法,直接响应给调用方,避免了调用超时时,长时间的等待。</p>
<h4>负载均衡</h4>
<p>前文已经提到 Feign 中已经默认集成了 Ribbon 功能,所以可以通过 Feign 直接实现负载均衡。启动两个 card-service 实例,打开 Nacos 控制台,发现实例已经注册成功。再通过 swagger-ui 或 PostMan 工具访问多访问几次 bindMobile 方法,通过控制台日志输出,可以发现请求在两个 card-service 实例中轮番执行。</p>
<p><img src="assets/2020-05-05-021623.jpg" alt="img" /></p>
<p>如何改变默认的负载均衡策略呢?先弄清楚 Ribbon 提供了几种负载策略:随机、轮询、重试、响应时间权重和最空闲连接,分别对应如下</p>
<blockquote>
<p>com.netflix.loadbalancer.RandomRule com.netflix.loadbalancer.RoundRobinRule com.netflix.loadbalancer.RetryRule com.netflix.loadbalancer.WeightedResponseTimeRule com.netflix.loadbalancer.BestAvailableRule</p>
</blockquote>
<p>由于是客户端负载均衡,必须配置在服务调用者项目中增加如下配置来达到调整的目的。</p>
<blockquote>
<p>card-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule</p>
</blockquote>
<p>也可以通过 Java 代码的形式调整,放置在项目启动类的下面</p>
<pre><code class="language-java"> @Bean
public IRule ribbonRule() {
// 负载均衡规则,改为随机
return new RandomRule();
// return new BestAvailableRule();
// return new WeightedResponseTimeRule();
// return new RoundRobinRule();
// return new RetryRule();
}
</code></pre>
<p>至此,我们通过一个&quot;会员绑定手机号,并增加会员相应积分&quot;的功能,通过两种方式完成一个正常的服务调用,并测试了客户端负载均衡的配置及使用。</p>
<h3>课外作业</h3>
<p>掌握了服务间调用后,在不考虑非业务功能的情况下,基本可以将本案例中大部分业务逻辑代码编写完成,可参照自己拆解的用户故事,或者主要的业务流程图,现在就动手,把代码完善起来吧。</p>
<p>服务调用是微服务间频繁使用的功能,选定一个简捷的调用方式尤其重要。照例留下一道思考题吧:本文用到了客户端负载均衡技术,它与我们时常提到的负载均衡技术有什么不同吗?</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/SpringCloud微服务实战/06 服务多不易管理如何破——服务注册与发现.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/SpringCloud微服务实战/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html">下一页</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":"709975957d783d60","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>