服务注册与远程调用
服务注册与远程调用
一、介绍
在拆分黑马商城的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service服务,导致我们无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。
那么问题来了:我们该如何跨服务调用,准确的说,如何在cart-service中获取item-service服务中的提供的商品数据呢?
大家思考一下,我们以前有没有实现过类似的远程查询的功能呢?
答案是肯定的,我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items这个接口发起的请求:
1、RestTemplate
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
RestTemplate使用的基本步骤如下
- 注册RestTemplate到Spring容器
- 调用RestTemplate的API发送请求,常见方法有:
- getForObject:发送Get请求并返回指定类型对象
- PostForObject:发送Post请求并返回指定类型对象
- put:发送PUT请求
- delete:发送Delete请求
- exchange:发送任意类型请求,返回ResponseEntity
先将RestTemplate注册为一个Bean:
package com.hmall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RemoteCallConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2、远程调用
接下来,我们修改cart-service中的com.hmall.cart.service.impl.``CartServiceImpl的handleCartItems方法,发送http请求到item-service:
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
//1、利用restTemplate 发起HTTP请求,得到HTTP响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://192.168.175.129:8081/items?ids={ids}", //请求路径
HttpMethod.GET, //请求方法
null, //请求实体
new ParameterizedTypeReference<List<ItemDTO>>() {}, //返回值类型
Map.of("ids", CollUtils.join(itemIds, ",")) //请求参数
);
//2、解析响应
if(!response.getStatusCode().is2xxSuccessful()){
return;
}
List<ItemDTO> items = response.getBody();
可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:
请求方式
请求路径
请求参数
返回值类型
3、缺点
假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署此时,每个item-service的实例其IP或端口不同,问题来了:
- item-service这么多实例,cart-service如何知道每一个实例的地址?
- http请求要写url地址,
cart-service服务到底该调用哪个实例呢? - 如果在运行过程中,某一个
item-service实例宕机,cart-service依然在调用该怎么办? - 如果并发太高,
item-service临时多部署了N台实例,cart-service如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了
二、服务注册(Nacos)
1、注册中心原理
在微服务远程调用的过程中,包括两个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service - 服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:

流程如下
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
调用者自己对实例列表负载均衡,挑选一个实例
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
2、Nacos注册中心
目前开源的注册中心框架有很多,国内比较常见的有
- Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
- Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
- Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言
1)配置MySQL
我们基于Docker来部署Nacos的注册中心,首先我们要准备MySQL数据库表,用来存储Nacos的数据,mysql脚本可以从github下载

2)修改配置文件
创建一个nacos/custom.env文件,MYSQL_SERVICE_HOST也就是mysql地址,需要修改为你自己的虚拟机IP地址:
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.175.129
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
3)创建镜像
然后,将刚刚创建的的nacos目录上传至虚拟机的/root目录。进入root目录,然后执行下面的docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
启动完成后,访问下面地址:http://192.168.175.129:8848/nacos/,注意将192.168.175.129替换为你自己的虚拟机IP地址。
首次访问会跳转到登录页,账号密码都是nacos
3、服务注册
接下来,我们把item-service注册到Nacos
步骤如下:
- 引入依赖
- 配置Nacos地址
- 重启
1)添加依赖
在item-service的pom.xml中添加依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)配置Nacos
在item-service的application.yml中添加nacos地址配置:
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
3)启动
然后配置启动项,注意重命名并且配置新的端口,避免冲突。
VM options: -Dserver.port=8083
访问nacos控制台,可以发现服务注册成功。
4、服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现
步骤如下:
- 引入依赖
- 配置Nacos地址
- 发现并调用服务
1)引入依赖
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
我们在cart-service中的pom.xml中添加下面的依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)配置Nacos地址
在cart-service的application.yml中添加nacos地址配置:
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
3)发现并调用服务
接下来,服务调用者cart-service就可以去订阅item-service服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
- 随机
- 轮询
- IP的hash
- 最近最少访问
- ...
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
//2.1、根据服务名称获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if (CollUtils.isEmpty(instances)) {
return;
}
//2.2、手写负载均衡,从实例列表中选择一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size())); //随机负载均衡
//2.3.1、利用restTemplate 发起HTTP请求,得到HTTP响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri() + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtils.join(itemIds, ","))
);
//2.3.2、解析响应
if(!response.getStatusCode().is2xxSuccessful()){
return;
}
List<ItemDTO> items = response.getBody();
三、远程调用(OpenFeign)
我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。
其实远程调用的关键点就在于四个:
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
1、快速入门
1)引入依赖
在cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖:
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2)启用OpenFeign
接下来,我们在cart-service的CartApplication启动类上添加注解,启动OpenFeign功能:
@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
3)编写OpenFeign客户端
在cart-service中,定义一个新的接口,编写Feign客户端:
其中代码如下:
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法
接口中的几个关键信息
@FeignClient("item-service"):声明服务名称@GetMapping:声明请求方式@GetMapping("/items"):声明请求路径@RequestParam("ids") Collection<Long> ids:声明请求参数List<ItemDTO>:返回值类型
4)使用FeignClient
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作。
2、连接池
Feign底层发起http请求,依赖于其它的框架。
其底层支持的http客户端实现包括
- HttpURLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
1)引入依赖
在cart-service的pom.xml中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2)开启连接池
在cart-service的application.yml配置文件中开启Feign的连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。
3、结构优化
如果拆分了交易微服务(trade-service),它也需要远程调用item-service中的根据id批量查询商品功能。这个需求与cart-service中是一样的。因此,我们就需要在trade-service中再次定义ItemClient接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?
1)思路分析
避免重复编码的办法就是抽取。不过这里有两种抽取思路:
- 思路1:抽取到微服务之外的公共module
- 思路2:每个微服务自己抽取一个module

方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
2)抽取Feign客户端
在hmall下定义一个新的module,命名为hm-api,然后把ItemDTO和ItemClient都拷贝过来,最终结构如下:

现在,任何微服务要调用item-service中的接口,只需要引入hm-api模块依赖即可,无需自己编写Feign客户端了。
接下来,我们在cart-service的pom.xml中引入hm-api模块:
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
启动类上添加声明
在cart-service的启动类上添加声明
@EnableFeignClients(basePackages = "com.hmall.api.client")
或者声明要用的FeignClient
@EnableFeignClients(clients= {ItemClient.class})
否则会扫描不到ItemClient,从而报错
4、日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
1)定义日志级别
在hm-api模块下新建一个配置类,定义Feign的日志级别:
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
2)配置
接下来,要让日志级别生效,还需要配置这个类。有两种方式:
- 局部生效:在某个
FeignClient中配置,只对当前FeignClient生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
- 全局生效:在
@EnableFeignClients中配置,针对所有FeignClient生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)