• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
打工人丶
博客园    首页    新随笔    联系   管理    订阅  订阅

SpringCloud Alibaba-4-Feign远程调用

远程调用:在分布式系统中,我们使用springboot创建了各种各样服务,那么这些服务之间如何进行远程调用呢。如:订单微服务怎么去调用商品微服务?


Ribbon:是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。

Ribbon支持的负载均衡策略:

  • BestAvailableRule

  • AvailabilityFilteringRule

  • WeightedResponseTimeRule

  • RetryRule

  • RoundRobinRule

  • RandomRule

  • ZoneAvoidanceRule(默认)



什么是负载均衡?

负载均衡:就是将访问请求进行分摊到多个服务器上进行执行。

分类:

  • 服务端负载均衡——————Ngix
  • 客户端负载均衡——————Nacos

微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。

    服务端负载均衡:指的是发生在服务提供者一方。
        比如:nginx负载均衡。请求到达服务器时,负载均衡器会根据预先设定的算法将请求分配到不同的服务器上,以达到均衡负载的目的。

    客户端负载均衡:指的是发生在服务请求的一方。
        比如:Ribbon负载均衡。请求会通过先某种算法来决定选择哪个可用的服务器,然后将请求发送到选定的服务器上,以达到均衡负载的目的。






1. Feign

Feign:Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。

Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。







2. 使用Feign,我们以 SpringCloud Alibaba-3-注册/配置中心 为例,实现用户下单的远程调用

Feign接口定义要点:

  • @FeignClient(name = "xxxx") 中name为服务提供者在nacos上注册的服务名, 否则报错。

  • @GetMapping("/products/{pid}") 指定接口路径,必须跟服务提供者提供接口url一样,否则报错。

  • 定义接口参数:如果使用了参数路径方式访问,需要使用@PathVariable("pid") 明确指定路径参数,否则报错。

  • 定义接口参数:如果使用普通方式访问,参数需要使用@RequestParam标记,否则报错。

  • 定义接口参数:如果是对象参数,参数需要使用@RequestBody标记(注意fegin接口,controler接口都要),否则报错。

  • 定义接口参数:如果需要进行文件上传,需要使用@RequestPart注解标记。

  • 如果只有一个参数,在controller里面的时候,可以省略,直接写@PathVariable。但是在定义Feign接口方法的时候,方法参数上不能省略@PathVariable(里面的值)



Feign超时时间配置:
Feign默认是有连接时间的(默认10s),还有请求超时时间(默认60s)。

  • 连接时间是指:A服务连接到B服务的时间。
  • 超时时间是指:A服务发了请求,B服务响应请求返回给A服务的时间。

超时后会报:java.net.SocketTimeoutException: Read timed out


注意:虽然feign的超时时间比较充足,但是你会发现当你的连接超时超过1秒,就会报错。这是因为Feign集成了ribbon,而在Ribbon中,默认的连接超时时间是(1秒),默认的请求超时时间是 (2秒)。

怎么解决超时时间呢:
    fengn集成ribbon后,超时机制原理:如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。
    理解了超时设置的原理,由之产生两种方案也是很明了了,如下: 
        1. 设置openFeign的超时时间(推荐) 
        2. 设置Ribbon的超时时间

配置openFeign超时时间:

  1. 方法一:通过创建配置类【可一次性配置用于全局】
  2. 方法二:通过调整配置文件 .yml【只能用于单个单个服务配置】

方法一,用过配置类(可局部,可全局)

@Configuration
public class FeignConfig{
    @Bean
    public Request.Options opt(){
        return new Request.Options(5000,10000)      // 参数1:连接超时时间  // 参数2:请求响应超时时间       
    }
}

方法二,通过配置文件(可局部,可局部)

# 局部
feign:
  client:
    config:
      xxxx: # Feign的客户端名称(要被调用的服务所对应的服务名,@FeignClient(name = "xxxxxoooooo")  name所对应的值)
        connectTimeout: 5000   # 连接超时时间
        readTimeout: 10000    # 请求响应超时时间

# 全局
feign:
  client:
    config:
      default: # default设置的是全局超时时间,对所有的openFeign接口服务都生效
        connectTimeout: 5000   # 连接超时时间
        readTimeout: 10000    # 请求响应超时时间

      xxxx: # Feign的客户端名称(要被调用的服务所对应的服务名,@FeignClient(name = "xxxxxoooooo")  name所对应的值)
        connectTimeout: 5000   # 连接超时时间
        readTimeout: 10000    # 请求响应超时时间
      
      # 如果同时配置全局和局部的,局部的会覆盖全局的


Feign相关属性配置:

在Feign中,若希望对单个指定特定名称的 Feign 进行配置
配置示例如下:
feign:
  client:
    config:
      feignName:  # Feign的客户端名称(要被调用的服务所对应的服务名,@FeignClient(name = "xxxxxoooooo")  name所对应的值)
        connectTimeout: 5000  # 连接超时时间
        readTimeout: 5000     # 读超时时间设置
        loggerLevel: full     # 配置Feign的日志级别
        errorDecoder: com.example.SimpleErrorDecoder   # Feign的错误解码器
        retryer: com.example.SimpleRetryer    # 配置重试
        requestInterceptors:  # 配置拦截器
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder    # Feign的编码器
        decoder: com.example.SimpleDecoder    # Feign的解码器
        contract: com.example.SimpleContract  # Feign的Contract配置

///////////////////////////////////////

在Feign中,若希望对所有 Feign 进行配置
配置示例如下:
feign:
  client:
    config:
      default:
        readTimeout: 5000
        loggerLevel: full
        connectTimeout: 5000

@EnableFeignClients 注解上有个 defaultConfiguration 属性,可以将默认配置统一写在一个配置类中。该配置方式同样可以作用于所有 Feign。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) // 作用于全局
public class FeignApplication {

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

}

# 特别注意:如果通过 Java 代码的方式配置过 Feign,然后又通过 application.yml 或者 application.properties 属性文件的方式配置 Feign,默认情况下属性文件中 Feign 的配置会覆盖 Java 代码的配置。
# 但是可以通过使用参数 feign.client.default-to-properties=false 来改变 Feign 配置生效的优先级。


Feign的日志配置:
有时候我们遇到接口调用失败,想看看问题原因或者调用性能。就需要看Feign日志,Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,可以在配置中开启日志。
默认是不开启Feign日志的。 且需要项目的日志级别是DEBUG,所以如果是INFO的话,就算你开启Feign的日志你也看不到。

Feign的日志级别:

  • NONE【性能最佳,适用于生产】:不记录任何日志(默认值)。
  • BASIC【适用于生产环境】:仅记录请求方法,URL,相应状态码及执行时间。
  • HEADERS:在BASIC级别的基础上,还记录请求和响应的header。
  • FULL:【适用于开发环境】:在HEADERS级别的基础上,还记录body和元数据。

开启Feign日志:

  1. 方法一:通过创建配置类【可一次性配置用于全局】
  2. 方法二:通过调整配置文件 .yml 【只能用于单个单个服务配置】

方法一,通过配置类(可全局,可局部)

    // 如果在类上加了@Configuration注解,则会全局生效。或者在启动类上@EnableDiscoveryClient加 defaultConfiguration 属性指定该类。  
    // 如果不加,则表示你可能想只要调用指定服务时才输出日志。  那么你需要在你想要获得日志的服务上,追加 @FeignClient(value="xxxx", configuration = FeignConfig.class)
    @Configuration
    public class FeignConfig{
        @Bean
        public Logger.Level feignLoggerLevel(){
          return Logger.Level.FULL;
        }
    }

/////////////////////////////////////////////////////////////////

    // .yml 文件
    logging:
      level:
        com.lihao.api.ProductServiceApi: debug   // 因为springboot默认是INFO级别的日志,是看不到Feign的日志的。需要将日志级别设置为DEBUG.

方法二,通过配置文件(可局部)

logging:
  level:
    com.lihao.api.ProductServiceApi: debug
feign:
  client:
    config:
      xxxx: # Feign的客户端名称(要被调用的服务所对应的服务名,@FeignClient(name = "xxxxxoooooo")  name所对应的值)
        loggerLevel: FULL



2.1 调整之前的代码,添加新功能,我们以 SpringCloud Alibaba-3-注册/配置中心 为例,实现用户下单的远程调用

shop-user模块:

	// 根据用户名查询用户信息
	@GetMapping("/getUserInfo/{username}")
	public User getUserInfo(@PathVariable String username){
		LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(User::getUsername, username);
		return userService.getOne(wrapper);
	}


shop-product模块:

	// 根据商品名称获取商品信息
	@GetMapping("/getProductInfo/{pname}")
	public Product getProductInfo(@PathVariable String pname){
		LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(Product::getPname, pname);
		return productService.getOne(wrapper);
	}
	
	// 保存或更新商品信息
	@PutMapping("/saveOrUpdate}")
	public void saveOrUpdate(@RequestBody Product product){
		productService.saveOrUpdate(product);
	}

2.2 引入Feign

shop-order模块:

  1. 添加Feign依赖
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

  1. 开启feign注解 @EnableFeignClients
@EnableFeignClients // 开启feign注解
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.lihao.mapper")
public class ShopOrderApplication {

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

}

  1. 创建Feign接口
//FeignClient 常用属性:
//name:指定Feign客户端的名称,用于区分不同的客户端。如果有多个服务名,可以用 , 或者 ; 分隔。            以我的理解,name和value这种属性,只有在你引入了注册中心的时候才有用
//value:与name属性作用相同,也是指定Feign客户端的名称。如果有多个服务名,可以用 , 或者 ; 分隔。         以我的理解,name和value这种属性,只有在你引入了注册中心的时候才有用
//url:指定远程服务的URL路径。该属性与 value 和 name 互斥,优先级更高。
//path:指定了 Feign 客户端请求远程服务的基础 URL 路径。相当于 controller 类上的@RequestMapping路径。必须以斜杠("/")开头
//configuration:指定自定义的 Feign 配置类,用于覆盖默认配置,常用于修改超时时间、重试次数等。
//fallback:指定服务调用失败时的容错处理类
//fallbackFactory:与fallback一样,它可以提供更加灵活的异常信息处理方式

@FeignClient(name = "service-product")
public interface ProductServiceApi {
	
	// 根据商品名称获取商品信息
	@GetMapping("/product/getProductInfo/{pname}")
	public Product getProductInfo(@PathVariable String pname);
	
	// 保存或更新商品信息
	@PutMapping("/product/saveOrUpdate}")
	public void saveOrUpdate(@RequestBody Product product);
}




@FeignClient(name = "service-user")
public interface UserServiceApi {
	
	// 根据用户名查询用户信息
	@GetMapping("/user/getUserInfo/{username}")
	public User getUserInfo(@PathVariable String username);
	
}

  1. 通过Feign远程调用
@RestController
@RequestMapping("/order")
public class OrderController {
	
	@Autowired
	private OrderService orderService;
	
	@GetMapping("/list")
	public List<Order> list(){
		return orderService.list();
	}
	
	@GetMapping("/makeOrder/{username}/{pname}/{number}")
	public Order makeOrder(@PathVariable String username,
	                       @PathVariable String pname,
	                       @PathVariable Integer number){
		return orderService.makeOrder(username, pname, number);
	}
}




@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
	
	@Resource
	private ProductServiceApi productServiceApi;
	
	@Resource
	private UserServiceApi userServiceApi;
	
	/**
	 * @param username 用户名
	 * @param pname    商品名
	 * @param number   数量
	 * @return com.lihao.entity.Order
	 * @author hx
	 * @date 2024/3/1 17:22
	 * @apiNote 创建订单
	 **/
	@Override
	public Order makeOrder(String username, String pname, Integer number) {
		
		// 1. 判断产品是否还有相应数量的产品
		Product product = productServiceApi.getProductInfo(pname);
		if (product==null) {
			throw new RuntimeException("该商品不存在");
		}
		
		int count = product.getStock() - number;
		if (count<number) {
			throw new RuntimeException("产品数量不足");
		}
		
		// 2. 获取用户信息
		User userInfo = userServiceApi.getUserInfo(username);
		if (userInfo==null) {
			throw new RuntimeException("该用户不存在");
		}
		
		// 3. 生成订单
		Order order = new Order();
		order.setUid(userInfo.getUid()); // 用户ID
		order.setUsername(username); // 用户名
		order.setPname(pname); // 商品名称
		order.setPprice(product.getPprice() * number); // 商品总价
		order.setNumber(number); // 购买数量
		this.saveOrUpdate(order);
		
		// 4. 更新产品信息
		product.setStock(count - number);
		productServiceApi.saveOrUpdate(product);
		
		return order;
	}
}






3. 查看Feign负载均衡显现

前要:开启2个商品客户端(localhost:8081,localhost:8082),这里说明一下,真实开发服务器可定是部署在不同的服务器中,ip不一样,端口可以一样。此时为学习,只有一台电脑,使用不同端口模拟一下。


	@Value("${server.port}")
	private String port;
	
	// 根据商品名称获取商品信息
	@GetMapping("/getProductInfo/{pname}")
	public Product getProductInfo(@PathVariable String pname){

		System.out.println("当前服务器端口号:"+port);

		LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
		wrapper.eq(Product::getPname, pname);
		return productService.getOne(wrapper);
	}

现象:成功实现了负载均衡。而且,Feign默认使用的是Rabbion的轮询策略。


我们可以通过修改配置来调整Ribbon的负载均衡策略。
product-service: # 调用的提供者的名称
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
posted @ 2024-03-01 20:30  &emsp;不将就鸭  阅读(255)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3