【Spring Cloud 微服务】——第二章 服务注册与发现和远程调用
目录
1. 服务注册和发现
1.1. 注册中心原理
1.2. Nacos 注册中心
1.3. 服务注册
1.3.1. 添加依赖
1.3.2. 配置 Nacos
1.3.3. 启动服务实例
1.4. 服务发现
1.4.1. 引入依赖
1.4.2. 配置 Nacos
1.4.3. 服务发现与负载均衡
2. OpenFeign
2.1. 快速入门
2.1.1. 引入依赖
2.1.2. 启用 OpenFeign
2.1.3. 编写 OpenFeign 客户端
2.1.4. 使用 FeignClient
2.2. 连接池
2.2.1. 引入依赖
2.2.2. 开启连接池
2.3. 最佳实践
2.3.1. 抽取 Feign 客户端
2.3.2. 扫描包
2.4. 日志配置
2.4.1. 定义日志级别
2.4.2. 配置
3. 总结
本文介绍了微服务架构中服务注册发现与远程调用的实现方案。首先阐述了手动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:88481.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 发起跨服务调用
