微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign

微服务技术栈导学?

什么是微服务?

微服务是分布式架构的一种。SpringCloud只是解决了服务拆分的服务治问题。

微服务技技术栈

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第1张图片

服务组成服务集群。
注册中心:记录微服务中每一个服务的ip、端口以及能干什么事。
配置中心:统一管理整个微服务里成千上百个服务配置。实现微服务热更新。
服务网关:请求路由、负载均衡
分布式缓存-〉分布式搜索
消息队列:异步通信组件,提高并发

解决服务的异常:

分布式日志服务:统计整个集群中的服务日志,统一存储统计分析。
系统监控链路追踪:可以实时监控整个集群中每个节点的集群状态,内存占用等等。可以快速定位到某个方法。
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第2张图片

微服务自动化部署:

Jenkins:对微服务项目进行自动化的编译
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第3张图片

微服务学习路线

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第4张图片

认识微服务

微服务架构的演变

单体架构

单体架构:将业务的所有功能集中在一个项目中开发,打包成一个包部署。
优点:架构简单,部署成本低
缺点:耦合度高

分布式架构

分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务。
优点:降低服务耦合,有利于服务升级拓展
缺点:架构复杂,难度大。

微服务

微服务的架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

微服务:一种良好的分布式架构方案。
优点:筛分粒度更小,服务更独立,耦合度更低。
缺点:架构复杂,运维、监控,部署难度提高。

微服务技术

微服务技术:Spring Cloud 和阿里巴巴的 Dubbo

微服务技术对比

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第5张图片

企业需求

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第6张图片

Spring Cloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第7张图片

服务的拆分及远程调用

服务拆分注意事项

  1. 不同微服务不要重复开发相同业务。
  2. 微服务数据独立,不要访问其他微服务的数据库。
  3. 微服务可以将自己的业务暴露为接口供其他微服务调用。

微服务远程调用

  1. 注册RestTemplate

在order-service的OrderApplication中注册RestTemplate

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * 创建RestTemplate并注入Spring容器
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  1. 服务远程调用RestTemplate

修改order-service中的OrderService的queryOrderById方法:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1.url路径
        String url = "http://localhost:8081/user/" + order.getUserId();
        // 2.2.发送http请求,实现远程调用
        User user = restTemplate.getForObject(url, User.class);
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

微服务调用方式

  • 基于RestTemplate发起的http请求实现远程调用
  • http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。

提供者和消费者

服务调用关系

  • 服务提供者:暴露接口给其它微服务调用
  • 服务消费者:调用其它微服务提供的接口
  • 提供者与消费者角色其实是相对的
  • 一个服务可以同时是服务提供者和服务消费者

Eureka注册中心

Eureka 的作用

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第8张图片

  1. 注册服务信息
  2. 拉取服务信息
  3. 负载均衡
  4. 远程调用

消费者该如何获取服务提供者具体信息?

  • 服务提供者启动时向Eureka注册自己的信息
  • Eureka保存这些信息
  • 消费者根据服务名称向eureka拉取提供者信息

如果有多个服务提供者,消费者该如何选择?

  • 服务消费者利用负载均衡算法,从服务列表中挑选一个

消费者如何感知服务提供者健康状态?

  • 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
  • Eureka会更新记录服务列表信息,心跳不正常会被剔除
  • 消费者就可以拉取到最新的信息

在Eureka架构中,微服务角色有两类:

  • EurekaServer:服务端,注册中心
    • 记录服务信息
    • 心跳监控
  • EurekaClient:客户端
    • Provider:服务提供者,例如案例中的 user-service
      • 注册自己的信息到EurekaServer
      • 每隔30秒向EurekaServer发送心跳
    • consumer:服务消费者,例如案例中的
      • order-service 根据服务名称从EurekaServer拉取服务列表
      • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

Eureka实战(代码未贴)

1. 搭建EurekaServer

  • 引入eureka-server依赖
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>
  • 添加@EnableEurekaServer注解
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
  • 在application.yml中配置eureka地址
server:
  port: 10086 # 服务端口
spring:
  application:
    name: eurekaserver # eureka的服务名称
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

2. 服务注册 user-service

  • 引入eureka-client依赖

同上

  • 在application.yml中配置eureka地址
server:
  port: 8081
spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
mybatis:
  type-aliases-package: cn.itcast.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.itcast: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka
pattern:
  name: 本地环境local

将user-service多次启动, 模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第9张图片-Dserver.port=8082

3. 服务发现

  • 引入eureka-client依赖(同上)
  • 在application.yml中配置eureka地址
server:
  port: 8088
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice
mybatis:
  type-aliases-package: cn.itcast.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.itcast: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka
  • 给RestTemplate添加@LoadBalanced注解:标记这个注解的方法要被Ribbon拦截
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * 创建RestTemplate并注入Spring容器
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  • 用服务提供者的服务名称远程调用
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1.url路径
        String url = "http://userservice/user/" + order.getUserId();
        // 2.2.发送http请求,实现远程调用
        User user = restTemplate.getForObject(url, User.class);
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

localhost:8088/order/101

Ribbon负载均衡

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第10张图片
前面给RestTemplate添加@LoadBalanced注解:标记这个注解的方法要被Ribbon拦截
拦截过程由LoadBalancerInterceptor完成。
LoadBalancerInterceptor实现了ClientHttpRequestInterceptor接口。
ClientHttpRequestInterceptor接口:客户端http请求的拦截器。intercept方法。

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();// 找到服务名:userservice 完成服务的拉取
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    // loadBalancer为 RibbonLoadBalancerClient(Ribbon负载均衡客户端)
    return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
    ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId); // 根据服务名称找Eureka拉取服务列表
    // loadBalancer对象为DynamicServerListLoadBalancer(动态服务列表负载均衡器) upServerList为
    // 负载均衡
    Server server = this.getServer(loadBalancer, hint);
    // 底层调用 chooseServer
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    } else {
        RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
        return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
    }
}
    protected IRule rule;
	public Server chooseServer(Object key) {
        if (this.counter == null) {
            this.counter = this.createCounter();
        }

        this.counter.increment();
        if (this.rule == null) {
            return null;
        } else {
            try {
                return this.rule.choose(key);
            } catch (Exception var3) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
                return null;
            }
        }
    }

ctrl+h:发现IRule的实现类微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第11张图片
IRule:负载均衡策略

负载均衡原理

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第12张图片

负载均衡策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第13张图片

不同规则的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

默认的实现就是ZoneAvoidanceRule,是一种轮询方案

自定义负载均衡策略

通过定义IRule实现可以修改负载均衡规则,有两种方式:

  1. 代码方式(负载均衡范围是全体):在order-service中的OrderApplication类中,定义一个新的IRule:
	@Bean
    public IRule randomRule() {
        return new RandomRule();
    }
  1. 配置文件方式(只针对某个服务而言):在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice:  # 指定服务
	ribbon:
		NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # 负载均衡规则

注意,一般用默认的负载均衡规则,不做修改。

饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称
      - userservice
      - xxservice

Nacos注册中心

国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。

服务注册到nacos

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:

  • 依赖不同
  • 服务地址不同

1)引入依赖

在cloud-demo父工程的pom文件中的中引入SpringCloudAlibaba的依赖:

<dependency>
  <groupId>com.alibaba.cloudgroupId>
  <artifactId>spring-cloud-alibaba-dependenciesartifactId>
  <version>2.2.6.RELEASEversion>
  <type>pomtype>
  <scope>importscope>
dependency>

然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:

<dependency>
  <groupId>com.alibaba.cloudgroupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>

注意:不要忘了注释掉eureka的依赖。

2)配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:

spring:
	cloud:
		nacos:
			server-addr: localhost:8848

注意:不要忘了注释掉eureka的地址

3)重启

重启微服务后,登录nacos管理页面,可以看到微服务信息:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第14张图片

服务分级存储模型

一个服务可以有多个实例,例如我们的user-service,可以有:

  • 127.0.0.1:8081
  • 127.0.0.1:8082
  • 127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例划分为一个集群
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第15张图片微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第16张图片杭州机房内的order-service应该优先访问同机房的user-service。

给user-service配置集群

修改user-service的application.yml文件,添加集群配置:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

重启两个user-service实例后,我们可以在nacos控制台看到下面结果
我们再次复制一个user-service启动配置,添加属性:
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第17张图片
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第18张图片

同集群优先的负载均衡

默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。
1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 

NacosRule负载均衡策略

  1. 优先选择同集群服务实例列表
  2. 本地集群找不到提供者,才去其它集群寻找,并且会报警告
  3. 确定了可用实例列表后,再采用随机负载均衡挑选实例

权重配置

实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:

注意:如果权重修改为0,则该实例永远不会被访问

实例的权重控制

  1. Nacos控制台可以设置实例的权重值,0~1之间
  2. 同集群内的多个实例,权重越高被访问的频率越高
  3. 权重设置为0则完全不会被访问

环境隔离

Nacos提供了namespace来实现环境隔离功能。

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第19张图片

创建namespace

默认情况下,所有service、data、group都在同一个namespace,名为public:
命名空间创建,命名空间id为回头配置文件的namespace

给微服务配置namespace

给微服务配置namespace只能通过修改配置来实现。
例如,修改order-service的application.yml文件:

server:
  port: 8088
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
      discovery:
        cluster-name: SH # 集群名称
        namespace: 69d3cb14-70ae-47a5-be73-d4c6005a5843

访问 localhost:8088/order/102

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第20张图片
-Dserver.port=8083
-Dspring.cloud.nacos.discovery.cluster-name=SH
-Dspring.cloud.nacos.discovery.namespace=69d3cb14-70ae-47a5-be73-d4c6005a5843
image.png

Nacos和Eureka区别

Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第21张图片
服务提供者启动时都会把服务信息提供给注册中心。注册中心存储服务信息。
当消费者需要消费时,找注册中心拉取服务信息。拉取后将拉取到的信息缓存到服务列表中。定时拉取。
消费者拿到服务列表以后再去负载均衡挑选一个发起调用即可。
Nacos和Eureka差别:

  • 服务提供者的健康检测:

    • nacos将服务提供者划分成临时实例和非临时实例。
    • 临时实例采用心跳检测,非临时实例为nacos主动检测
    • 挂了以后 非临时实例不会从列表中剔除。
  • Nacos与eureka的共同点

    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测
  • Nacos与Eureka的区别

    • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时 pull+push
    • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

临时实例和非临时实例

服务注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:

server:
  port: 8088
spring:
  application:
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
      discovery:
        ephemeral: false # 是否是临时实例
        cluster-name: SH # 集群名称
        namespace: 69d3cb14-70ae-47a5-be73-d4c6005a5843

ephemeral:是否是临时实例。
临时实例宕机时,会从nacos的服务列表中剔除,而非临时实例则不会

Nacos配置管理

Nacos除了可以做注册中心,同样可以做配置管理来使用。

统一配置管理

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第22张图片
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。

在nacos中添加配置文件

配置列表中添加:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第23张图片

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

pattern:
  dateformat: yyyy-MM-dd HH:mm:ss

从微服务拉取配置

微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第24张图片
1)引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:


<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>

2)添加bootstrap.yaml
然后,在user-service中添加一个bootstrap.yaml文件,内容如下:

spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev #开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名

这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}- spring.application.name{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。
本例中,就是去读取userservice-dev.yaml:
3)读取nacos配置
在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
     @Value("${pattern.dateformat}")
     private String dateformat;

    @GetMapping("now")
    public String now(){
//        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));

    }
}

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第25张图片

配置热更新

我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新
要实现配置热更新,可以使用两种方式:

方式一:@RefreshScope

在@Value注入的变量所在类上添加注解@RefreshScope:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.cloud.context.config.annotation.RefreshScope;

@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
     @Value("${pattern.dateformat}")
     private String dateformat;

    @GetMapping("now")
    public String now(){
//        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));

    }
}

方式二:@ConfigurationProperties代替@Value

使用@ConfigurationProperties注解代替@Value注解。
在user-service服务中,添加一个类,读取patterrn.dateformat属性:

package cn.itcast.user.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

UserController类

import cn.itcast.user.config.PatternProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private PatternProperties patternProperties;
    @GetMapping("prop")
    public PatternProperties properties(){
        return properties;
    }

    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }
}

配置共享

其实微服务启动时,会去nacos读取多个配置文件,例如:

  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

[spring.application.name].yaml不包含环境,因此可以被多个环境共享。

1)添加一个环境共享配置

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第26张图片

2)在user-service中读取共享配置

在user-service服务中,修改PatternProperties类,读取新添加的属性:

3)运行两个UserApplication,使用不同的profile

修改UserApplication2这个启动项,改变其profile值:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第27张图片

配置共享的优先级

当nacos、服务本地同时出现相同属性时,优先级有高低之分:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第28张图片
微服务会从nacos读取的配置文件:

  • [服务名]-[spring.profile.active].yaml,环境配置
  • [服务名].yaml,默认配置,多环境共享

优先级:

  • [服务名]-[环境].yaml >[服务名].yaml > 本地配置

多服务共享配置

不同微服务之间可以共享配置文件,通过下面的两种方式来指定:

  1. 通过shared-configs指定
spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
      discovery:
        cluster-name: HZ
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 多微服务间共享的配置列表
          - dataId: common.yaml # 要共享的配置文件id
  1. 通过extension-configs指定
spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
      discovery:
        cluster-name: HZ
      config:
        file-extension: yaml # 文件后缀名
        extends-configs: # 多微服务间共享的配置列表
          - dataId: extend.yaml # 要共享的配置文件id

多种配置的优先级:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第29张图片

搭建Nacos集群(未完)

Nacos生产环境下一定要部署为集群状态,部署方式参考课前资料中的文档:
https://www.bilibili.com/video/BV1LQ4y127n4?p=29&spm_id_from=pageDriver&vd_source=33c29b1f252296faf7d8d0f7b4383c3a

Feign远程调用

先来看我们以前利用RestTemplate发起远程调用的代码:
存在下面的问题:

  • 代码可读性差,编程体验不统一
  • 参数复杂URL难以维护

Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。

定义和使用Feign客户端

Fegin的使用步骤如下:

1)引入依赖

我们在order-service服务的pom文件中引入feign的依赖:

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-openfeignartifactId>
dependency>

2)添加注解

在order-service的启动类添加注解开启Feign的功能:@EnableFeignClients
@EnableFeignClients(basePackages = {“com.qdsg”})

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

3)编写Feign的客户端

在order-service中新建一个接口,内容如下:

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:

  • 服务名称:userservice
  • 请求方式:GET
  • 请求路径:/user/{id}
  • 请求参数:Long id
  • 返回值类型:User

这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。

4)用Feign客户端代替RestTemplate

修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.用Feign远程调用
        User user = userClient.findById(order.getUserId());
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }

5)总结

使用Feign的步骤:

  1. 引入依赖
  2. 添加@EnableFeignClients注解
  3. 编写FeignClient接口
  4. 使用FeignClient中定义的方法代替RestTemplate

自定义配置

Feign可以支持很多的自定义配置,如下表所示:

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign. Contract 支持的注解格式 默认是SpringMVC的注解
feign. Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
下面以日志为例来演示如何自定义配置。

配置文件方式

  1. 基于配置文件修改feign的日志级别可以针对单个服务:(局部配置)
feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 
  1. 针对所有服务:(全局配置 default)
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Java代码方式

也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class) 

局部生效:把它放到@FeignClient这个注解中:

@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class) 

Feign性能优化

Feign底层发起http请求,依赖于其它的框架。
Feign底层的客户端实现:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此优化Feign的性能主要包括:

  • 使用连接池代替默认的URLConnection
  • 日志级别,最好用basic或none

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

Feign的性能优化-连接池配置

这里我们用Apache的HttpClient来演示。

1)引入依赖

在order-service的pom文件中引入Apache的HttpClient依赖:


<dependency>
    <groupId>io.github.openfeigngroupId>
    <artifactId>feign-httpclientartifactId>
dependency>
2)配置连接池

在order-service的application.yml中添加配置:

feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数
3)测试FeignClientFactoryBean的client

接下来,在FeignClientFactoryBean中的loadBalance方法中打断点:

    protected <T> T loadBalance(Builder builder, FeignContext context, HardCodedTarget<T> target) {
        Client client = (Client)this.getOptional(context, Client.class);
        if (client != null) {
            builder.client(client);
            Targeter targeter = (Targeter)this.get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        } else {
            throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
        }
    }

Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:
微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第30张图片

总结

Feign的优化:

  • 日志级别尽量用basic
  • 使用HttpClient或OKHttp代替URLConnection
    • 引入feign-httpClient依赖
    • 配置文件开启httpClient功能,设置连接池参数

最佳实践

  1. 继承方式
    1. 定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
    2. Feign客户端和Controller都集成改接口

微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第31张图片

  1. 抽取方式
    1. 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
      例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
      微服务实战--基础篇:Eureka、Ribbon、Nacos、Feign_第32张图片

你可能感兴趣的:(微服务,微服务,eureka,ribbon)