【Spring Cloud 微服务】——第二章 服务注册与发现和远程调用
目录1. 服务注册和发现1.1. 注册中心原理1.2. Nacos 注册中心1.3. 服务注册1.3.1. 添加依赖1.3.2. 配置 Nacos1.3.3. 启动服务实例1.4. 服务发现1.4.1. 引入依赖1.4.2. 配置 Nacos1.4.3. 服务发现与负载均衡2. OpenFeign2.1. 快速入门2.1.1. 引入依赖2.1.2. 启用 OpenFeign2.1.3. 编写 OpenFeign 客户端2.1.4. 使用 FeignClient2.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 注册中心目前开源的注册中心框架有很多EurekaNetflix 公司出品目前被集成在 Spring Cloud 当中NacosAlibaba 公司出品目前被集成在 Spring Cloud Alibaba 中ConsulHashiCorp 公司出品目前集成在 Spring Cloud 中由于 Nacos 是国内产品中文文档比较丰富而且同时具备配置管理功能因此在国内使用较多。官方网站Redirecting to: https://nacos.io/基于 Docker 来部署 Nacos 的注册中心首先需要准备 MySQL 数据库表用来存储 Nacos 的数据。然后执行以下命令启动 Nacosdocker run -d \ --name nacos \ --env-file ./nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --restartalways \ 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 groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency1.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 groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency1.4.2. 配置 Nacosspring: cloud: nacos: server-addr: localhost:88481.4.3. 服务发现与负载均衡服务发现需要用到一个工具DiscoveryClientSpring Cloud 已经自动装配可以直接注入使用。修改原来的远程调用之前调用时需要写死服务提供者的 IP 和端口// 写死地址 restTemplate.getForObject(http://localhost:8081/items/ id, ItemDTO.class);现在通过 DiscoveryClient 发现服务实例列表然后通过负载均衡算法选择实例Autowired private DiscoveryClient discoveryClient; public ItemDTO queryItemById(Long id) { // 1.查询服务列表 ListServiceInstance 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 groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency !--负载均衡器-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency2.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) ListItemDTO queryItemByIds(RequestParam(ids) CollectionLong ids); }接口中的关键信息FeignClient(item-service)声明服务名称GetMapping声明请求方式GetMapping(/items)声明请求路径RequestParam(ids) CollectionLong ids声明请求参数ListItemDTO返回值类型有了上述信息OpenFeign 就可以利用动态代理帮我们实现这个方法并且向 http://item-service/items 发送一个 GET 请求携带 ids 为请求参数并自动将返回值处理为ListItemDTO。2.1.4. 使用 FeignClient在 CartServiceImpl 中改造代码直接调用 ItemClient 的方法Service RequiredArgsConstructor public class CartServiceImpl extends ServiceImplCartMapper, Cart implements ICartService { private final ItemClient itemClient; Override public ListCartVO queryMyCarts() { // 1.查询我的购物车列表 ListCart carts lambdaQuery().eq(Cart::getUserId, getUserId()).list(); if (CollUtils.isEmpty(carts)) { return CollUtils.emptyList(); } // 2.转换VO ListCartVO vos BeanUtils.copyList(carts, CartVO.class); // 3.处理VO中的商品信息 handleCartItems(vos); // 4.返回 return vos; } private void handleCartItems(ListCartVO vos) { // 1.获取商品id SetLong itemIds vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 ListItemDTO items itemClient.queryItemByIds(itemIds); // 3.转为 id 到 item的map MapLong, 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 groupIdio.github.openfeign/groupId artifactIdfeign-okhttp/artifactId /dependency2.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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd parent artifactIddemo-parent/artifactId groupIdcom.example/groupId version1.0.0/version /parent modelVersion4.0.0/modelVersion artifactIdapi/artifactId dependencies !--open feign-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency !-- load balancer-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency !-- swagger 注解依赖 -- dependency groupIdio.swagger/groupId artifactIdswagger-annotations/artifactId version1.6.6/version scopecompile/scope /dependency /dependencies /project把 ItemDTO 和 ItemClient 都拷贝过来项目结构如下现在任何微服务要调用 item-service 中的接口只需要引入 api 模块依赖即可无需自己编写 Feign 客户端了。2.3.2. 扫描包在 cart-service 的 pom.xml 中引入 api 模块!--feign模块-- dependency groupIdcom.example/groupId artifactIdapi/artifactId version1.0.0/version /dependency删除 cart-service 中原来的 ItemDTO 和 ItemClient重启项目。如果报错了可能是因为 ItemClient 现在定义到了com.example.api.client包下而 cart-service 的启动类扫描不到。需要添加声明方式1声明扫描包ComponentScan(com.example.api)方式2声明要用的 FeignClientEnableFeignClients(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?idsxxx 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服务启动时会向注册中心注册自己的服务信息调用者可以从注册中心订阅服务获取服务对应的实例列表注册中心会定期检测服务健康状态剔除不健康实例远程调用OpenFeignOpenFeign 利用动态代理生成远程调用代码通过FeignClient注解声明服务名称使用 Spring MVC 注解声明请求方式、路径、参数和返回值支持连接池配置OKHttp可以抽取公共 API 模块避免重复编码支持灵活的日志配置Spring Cloud 微服务完整流程服务拆分将单体应用拆分为多个独立微服务服务注册每个微服务向 Nacos 注册中心注册自己服务发现消费者从 Nacos 订阅服务获取服务实例列表负载均衡对多个服务实例进行负载均衡远程调用使用 OpenFeign 发起跨服务调用