【Spring Cloud 微服务】——第二章 服务注册与发现和远程调用
目录
本文介绍了微服务架构中服务注册发现与远程调用的实现方案。首先阐述了手动HTTP调用存在的问题,进而引入注册中心(Nacos)的解决方案,详细说明了服务注册、发现、健康检查等核心机制。然后介绍了如何通过OpenFeign简化远程调用,使其如同本地方法调用,包括Feign客户端的定义、连接池优化、最佳实践(公共API模块抽取)以及日志配置。文章完整呈现了SpringCloud微服务架构的核心流程:服务拆分→注册→发现→负载均衡→远程调用,为构建弹性可扩展的分布式系统提供了实践指导。
1. 服务注册和发现
在上一章我们实现了微服务拆分,并且通过 Http 请求实现了跨微服务的远程调用。不过这种手动发送 Http 请求的方式存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,进行了多实例部署:

此时,每个 item-service 的实例其 IP 或端口不同,问题来了:
-
item-service 这么多实例,cart-service 如何知道每一个实例的地址?
-
http 请求要写 url 地址,cart-service 到底该调用哪个实例呢?
-
如果在运行过程中,某一个 item-service 实例宕机,cart-service 依然在调用该怎么办?
-
如果并发太高,item-service 临时多部署了 N 台实例,cart-service 如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了。
1.1. 注册中心原理
在微服务远程调用的过程中,包括两个角色:
-
服务提供者:提供接口供其它微服务访问,比如 item-service
-
服务消费者:调用其它微服务提供的接口,比如 cart-service
注册中心、服务提供者、服务消费者三者间关系如下:

流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时:
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
1.2. Nacos 注册中心
目前开源的注册中心框架有很多:
-
Eureka:Netflix 公司出品,目前被集成在 Spring Cloud 当中
-
Nacos:Alibaba 公司出品,目前被集成在 Spring Cloud Alibaba 中
-
Consul:HashiCorp 公司出品,目前集成在 Spring Cloud 中
由于 Nacos 是国内产品,中文文档比较丰富,而且同时具备配置管理功能,因此在国内使用较多。
官方网站:
Redirecting to: https://nacos.io/
基于 Docker 来部署 Nacos 的注册中心,首先需要准备 MySQL 数据库表,用来存储 Nacos 的数据。
然后执行以下命令启动 Nacos:
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://localhost:8848/nacos/,首次访问会跳转到登录页,账号密码都是 nacos。
1.3. 服务注册
接下来,我们把 item-service 注册到 Nacos。
1.3.1. 添加依赖
在 item-service 的 pom.xml 中添加依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1.3.2. 配置 Nacos
在 item-service 的 application.yml 中添加 nacos 地址配置:
1.3.3. 启动服务实例
配置多个 item-service 的部署实例,然后启动:

访问 nacos 控制台,可以发现服务注册成功:

点击详情,可以查看到 item-service 服务的多个实例信息:

1.4. 服务发现
服务的消费者要去 nacos 订阅服务,这个过程就是服务发现。
1.4.1. 引入依赖
在 cart-service 的 pom.xml 中添加依赖:
<!--nacos 服务发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1.4.2. 配置 Nacos
spring:
cloud:
nacos:
server-addr: localhost:8848
1.4.3. 服务发现与负载均衡
服务发现需要用到一个工具 DiscoveryClient,Spring Cloud 已经自动装配,可以直接注入使用。
修改原来的远程调用,之前调用时需要写死服务提供者的 IP 和端口:
// 写死地址
restTemplate.getForObject("http://localhost:8081/items/" + id, ItemDTO.class);
现在通过 DiscoveryClient 发现服务实例列表,然后通过负载均衡算法选择实例:
@Autowired
private DiscoveryClient discoveryClient;
public ItemDTO queryItemById(Long id) {
// 1.查询服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if (CollUtils.isEmpty(instances)) {
return null;
}
// 2.负载均衡,选择一个实例
ServiceInstance instance = instances.get(0); // 简化处理,可使用负载均衡算法
// 3.发送请求
String url = instance.getHost() + ":" + instance.getPort();
return restTemplate.getForObject("http://" + url + "/items/" + id, ItemDTO.class);
}
2. OpenFeign
在上一章,我们利用 Nacos 实现了服务的治理,利用 RestTemplate 实现了服务的远程调用。但是远程调用的代码太复杂了,而且与原本的本地方法调用差异太大。
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到 OpenFeign 组件了。
远程调用的关键点就在于四个:
-
请求方式
-
请求路径
-
请求参数
-
返回值类型
OpenFeign 利用 Spring MVC 的相关注解来声明上述 4 个参数,然后基于动态代理帮我们生成远程调用的代码,非常方便。
2.1. 快速入门
2.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.1.2. 启用 OpenFeign
在 cart-service 的 CartApplication 启动类上添加注解,启动 OpenFeign 功能:
@SpringBootApplication
@EnableFeignClients
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}
2.1.3. 编写 OpenFeign 客户端
在 cart-service 中,定义一个新的接口,编写 Feign 客户端:
package com.example.cart.client;
import com.example.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>:返回值类型
有了上述信息,OpenFeign 就可以利用动态代理帮我们实现这个方法,并且向 http://item-service/items 发送一个 GET 请求,携带 ids 为请求参数,并自动将返回值处理为 List<ItemDTO>。
2.1.4. 使用 FeignClient
在 CartServiceImpl 中改造代码,直接调用 ItemClient 的方法:
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final ItemClient itemClient;
@Override
public List<CartVO> queryMyCarts() {
// 1.查询我的购物车列表
List<Cart> carts = lambdaQuery().eq(Cart::getUserId, getUserId()).list();
if (CollUtils.isEmpty(carts)) {
return CollUtils.emptyList();
}
// 2.转换VO
List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);
// 3.处理VO中的商品信息
handleCartItems(vos);
// 4.返回
return vos;
}
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
}
Feign 替我们完成了服务拉取、负载均衡、发送 http 请求的所有工作,看起来优雅多了。
而且,这里不再需要 RestTemplate 了。
2.2. 连接池
Feign 底层发起 http 请求,依赖于其它框架。其底层支持的 http 客户端实现包括:
-
HttpURLConnection:默认实现,不支持连接池
-
Apache HttpClient:支持连接池
-
OKHttp:支持连接池
2.2.1. 引入依赖
在 cart-service 的 pom.xml 中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2.2.2. 开启连接池
在 cart-service 的 application.yml 配置文件中开启 Feign 的连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp功能
重启服务,连接池就生效了。
2.3. 最佳实践
将来我们要把与下单有关的业务抽取为一个独立微服务 trade-service,但如果每个微服务都自己定义 ItemClient 接口,就会有重复编码的问题。
有两种抽取思路:
-
思路1:抽取到微服务之外的公共 module
-
思路2:每个微服务自己抽取一个 module
方案1抽取更加简单,工程结构也比较清晰;方案2抽取相对麻烦,但服务之间耦合度降低。
2.3.1. 抽取 Feign 客户端
创建 Maven 模块 api:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo-parent</artifactId>
<groupId>com.example</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api</artifactId>
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- swagger 注解依赖 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
把 ItemDTO 和 ItemClient 都拷贝过来,项目结构如下:
现在,任何微服务要调用 item-service 中的接口,只需要引入 api 模块依赖即可,无需自己编写 Feign 客户端了。
2.3.2. 扫描包
在 cart-service 的 pom.xml 中引入 api 模块:
<!--feign模块-->
<dependency>
<groupId>com.example</groupId>
<artifactId>api</artifactId>
<version>1.0.0</version>
</dependency>
删除 cart-service 中原来的 ItemDTO 和 ItemClient,重启项目。
如果报错了,可能是因为 ItemClient 现在定义到了 com.example.api.client 包下,而 cart-service 的启动类扫描不到。需要添加声明:
-
方式1:声明扫描包:
@ComponentScan("com.example.api") -
方式2:声明要用的 FeignClient:
@EnableFeignClients(clients = ItemClient.class)
2.4. 日志配置
OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时,才会输出日志。而且其日志级别有 4 级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法、URL 以及响应状态码和执行时间
-
HEADERS:在 BASIC 的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
2.4.1. 定义日志级别
在 api 模块下新建一个配置类,定义 Feign 的日志级别:
package com.example.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
2.4.2. 配置
要让日志级别生效,还需要配置这个类。有两种方式:
-
局部生效:在某个 FeignClient 中配置,只对当前 FeignClient 生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class) -
全局生效:在
@EnableFeignClients中配置,针对所有 FeignClient 生效@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
日志格式示例:
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.example.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=xxx HTTP/1.1 17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.example.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms)
3. 总结
服务注册和发现(Nacos):
-
服务启动时会向注册中心注册自己的服务信息
-
调用者可以从注册中心订阅服务,获取服务对应的实例列表
-
注册中心会定期检测服务健康状态,剔除不健康实例
远程调用(OpenFeign):
-
OpenFeign 利用动态代理生成远程调用代码
-
通过
@FeignClient注解声明服务名称 -
使用 Spring MVC 注解声明请求方式、路径、参数和返回值
-
支持连接池配置(OKHttp)
-
可以抽取公共 API 模块避免重复编码
-
支持灵活的日志配置
Spring Cloud 微服务完整流程:
-
服务拆分:将单体应用拆分为多个独立微服务
-
服务注册:每个微服务向 Nacos 注册中心注册自己
-
服务发现:消费者从 Nacos 订阅服务,获取服务实例列表
-
负载均衡:对多个服务实例进行负载均衡
-
远程调用:使用 OpenFeign 发起跨服务调用
更多推荐

所有评论(0)