SpringCloud系列之五

服务通信与调用-Feign

1. 通过Feign进行远程调用的分析

先来看一下之前的调用方式

Eureka:http://ip:port/path

Ribbon:http://serviceName/path

1.1. Feign的内部调用方式

在这里插入图片描述

  • 引入Feign依赖的同时也引入两个组件

    • Ribbon:利用负载均衡器选定目标机器
    • Hystrix:根据熔断的开启状态,决定是否发起此次调用
  • 动态代理

    Feign是通过一个代理接口进行远程调用,这一步就是为了构造接口的动态代理对象,用来代理远程服务的真实调用,这样就能像调用本地方法一样调用HTTP请求,不需要像Ribbon或Eureka那样在方法调用的地方提供服务名。在Feign中动态代理是通过Feign.build返回的构造器来装配相关参数,这里就是应用的Builder设计模式。

  • Contract协议

    Feign有着自己的一套协议的规范,Contract协议会构造复杂的元数据对象MethodMetadata。

1.2. Feign发起调用的过程

在这里插入图片描述

  • 拦截器:是spring处理网络请求的经典方案,Feign也沿用了这个做法,通过一系列的拦截器多Request和Response对象进行装饰,构造请求头,装饰完毕后就正式发起调用
  • 发起请求
    • 重试:Feign这里借助Ribbon的配置重试机制实现重试操作,可以指定对当前服务节点发起重试,也可用让Feign换一个服务节点重试
    • 降级:Feign接口在声明的时候指定Hystrix的降级策略实现类,如果达到了Hystrix的超市判定,或者得到异常情况,将执行指定的降级逻辑

2. Feign远程调用实例

添加POM依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

实现启动类

package com.michael.springcloud;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FeignConsumerApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

创建调用的Service接口引用实现

package com.michael.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

//eureka-client是服务提供者的servie-name
//这个注解的意思是IService这个接口的抵用都发到eureka-client这个服务的提供者上
@FeignClient("eureka-client")
public interface IService {

    @GetMapping("/sayhello")
    public String sayHello();
}

controller的实现

package com.michael.springcloud.controller;

import com.michael.springcloud.service.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FeignController {

    @Autowired
    private IService service;

    @GetMapping("/sayhi")
    public String sayHi(){
        return service.sayHello();
    }
}

创建配置properties

spring.application.name=feign-consumer
server.port=20081
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/

3. 理想的Feign风格的项目结构

Feign的调用都是基于HTTP协议方案的

3.1. 抽取一个公共接口层

在feign目录下创建一个feign-client-intf

POM里仅保持最低限度的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

建立接口层

package com.michael.springcloud.service;

import com.michael.springcloud.pojo.PortInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

//这里不能再使用eureka-client了,要使用自己的服务提供方
@FeignClient("feign-client")
public interface IService {

    @GetMapping("/sayhello")
    String sayHello();

    @PostMapping("/sayhello")
    PortInfo sayPortInfo(@RequestBody PortInfo portInfo);
}

pojo实体对象

package com.michael.springcloud.pojo;

import lombok.Data;

@Data
public class PortInfo {

    private String name;
    private String port;
}

3.2. 新建服务提供者

在feign目录中创建项目feign-client

POM依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.michael</groupId>
            <artifactId>feign-client-intf</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

创建启动类

package com.michael.springcloud;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class FeignClientApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FeignClientApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

在controller里实现feign-client-intf的IService

package com.michael.springcloud.controller;

import com.michael.springcloud.pojo.PortInfo;
import com.michael.springcloud.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FeignController implements IService {

    @Value("${server.port}")
    private String port;

    @GetMapping("/sayhello")
    public String sayHello(){
        return "my port is "+port;
    }

    @PostMapping("/sayhello")
    public PortInfo sayPortInfo(@RequestBody PortInfo portInfo){
        log.info("you are "+portInfo.getName());
        portInfo.setPort(port);
        return portInfo;
    }
}

配置properties

spring.application.name=feign-client
server.port=20082
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/

3.3. 改进版的消费者

创建feign-consumer-advanced

设置POM文件

<?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>springcloud-learn</artifactId>
        <groupId>com.michael</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>feign-consumer-advanced</artifactId>
    <name>feign-consumer-advanced</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.michael</groupId>
            <artifactId>feign-client-intf</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

创建启动类

package com.michael.springcloud;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
//这个地方要注意IService所在的包路径,默认是扫com.michael.springcloud
//如果接口不在同一个包下,就需要把包扫进来
//@EnableFeignClients(basePackages"com.michael.*")
@EnableFeignClients
public class FeignConsumerAdvApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FeignConsumerAdvApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

controller实现

package com.michael.springcloud.controller;

import com.michael.springcloud.pojo.PortInfo;
import com.michael.springcloud.service.IService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FeignController {
    
    @Autowired
    private IService service;
    
    @GetMapping("/sayhi")
    public String sayHi(){
        return service.sayHello();
    }
    
    @PostMapping("/sayhi")
    public PortInfo sayHello(@RequestBody PortInfo portInfo){
        return service.sayPortInfo(portInfo);
    }
}

properties配置

spring.application.name=feign-consumer-advanced
server.port=20083
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/
# 配置允许注解的重载
spring.main.allow-bean-definition-overriding=true

4. 服务调用超时重试机制

# Feign接口的超时重试机制由Ribbon提供
feign-service-provider.ribbon.OkToRetryOnAllOperations=true
feign-service-provider.ribbon.ConnectTimeout=1000
feign-service-provider.ribbon.ReadTimeout=2000
feign-service-provider.ribbon.MaxAutoRetries=2
feign-service-provider.ribbon.MaxAutoRetriesNextServer=2

以上的参数设置都是对feign-service-provider这个微服务的超时重试策略

  • OkToRetryOnAllOperations:这个是指定了什么HTTP Method可以进行retry,这里设置的true的意思是无论GET还是POST都可以进行重试,一般来讲,在实际环境中往往只是GET请求才会允许重试,如果接口实现了幂等性后就可以进行重试了
  • ConnectTimeout:超时判定的第一个参数,单位是ms,创建会话的连接时间,注意这个不是服务的响应时间,而是本机和服务建立的一个Connection花费的时间,如果连接超时则直接进行重试
  • ReadTimeout:超时判断的第二个参数,服务响应时间。当连接建立后,如果对方服务没有在规定时间内返回,则直接进行重试
  • MaxAutoRetries:这里配置重试次数为2,那么在首次调用超时后,会再次向一个服务节点发起最多2次重试(总共向当前节点发起了1+2=3次请求)
  • MaxAutoRetriesNextServer:这里配置的2相当于换2个节点重试,在当前机器调用超时后,Feign将最多换N台机器发起调用(注意,这个参数将和MaxAutoRetries共同作用,也就是说,在新机器上超时后,会继续重试MaxAutoRetries+1次)

按照上面配置的参数,最大超时时间是多少?

(1000+2000)x(1+2)x(1+2)= 27000ms

极值函数的公式如下

MAX(Response Time) =(ConnectTimeout + ReadTimeout)*(MaxAutoRetries + 1)*(MaxAutoRetriesNextServer + 1)

5. 配置超时和重试机制

在feign-consumer-advanced里进行配置即可

# 调用feign-client这个服务

# 每台机器最大的重试次数
feign-client.ribbon.MaxAutoRetries=2
# 可以重试的机器数量
feign-client.ribbon.MaxAutoRetriesNextServer=2
# 连接的请求超时时长
feign-client.ribbon.ConnectTimeout=1000
# 业务处理的时长
feign-client.ribbon.ReadTimeout=2000
# HTTP Method的重试开关,可以配置GET、POST、PUT、DELETE
feign-client.ribbon.OkToRetryOnAllOperations=true

在feign-client-intf里增加一个测试接口

    @GetMapping("/retry")
    String retry(@RequestParam(name="timeout") int timeout);

在feign-client-advanced里实现这个接口

    @Override
    public String retry(@RequestParam(name="timeout") int timeout) {
        try{
            while (timeout-- >0){
                Thread.sleep(1000);
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        log.info("retry is "+port);
        return port;
    }

在feign-consumer-advanced的controller里实现

    @GetMapping("/retry")
    public String retry(@RequestParam(name="timeout") int timeout){
        return service.retry(timeout);
    }

启动顺序:eureka-server,3个feign-client-advanced,1个feign-consumer-advanced

调用:http://localhost:20083/retry?timeout=2

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值