SpringCloud Alibaba 微服务实战二十 - 集成 Feign 的降级熔断

本贴最后更新于 1344 天前,其中的信息可能已经事过景迁

在之前的项目中我们已经实现了使用 Feign 调用远程接口,本章内容主要是借助 sentinel 实现 Feign 接口熔断器功能。

概述

首先我们看看不使用熔断器的情况下调用一个没有启动的服务会出现什么效果,然后再来看看使用 sentinel 熔断器后的效果。

image.png

如上,我们使用 order-service 中 FeignController 调用 account-service 中的接口,在没启用熔断器的情况下,接口会抛出 500 异常。

实现

使用 sentinel 实现熔断器很简单,简单几步即可。

  1. 定义 fallback 类,当熔断时返回默认数据

    package com.javadaily.feign.fallback;
    @Slf4j
    public class AccountFeignFallback implements AccountFeign {
        @Setter
        private Throwable cause;
    
        @Override
        public ResultData<String> insert(AccountDTO accountDTO) {
            return ResultData.fail("接口熔断");
        }
    
        @Override
        public ResultData<String> delete(String accountCode) {
            return ResultData.fail("接口熔断");
        }
    
        @Override
        public ResultData<String> update(AccountDTO accountDTO) {
            return ResultData.fail("接口熔断");
        }
    
        @Override
        public ResultData<AccountDTO> getByCode(String accountCode) {
            log.error("查询失败,接口异常" ,cause);
            AccountDTO account = new AccountDTO();
            account.setAccountCode("000");
            account.setAccountName("测试Feign");
            return ResultData.success(account);
        }
    
        @Override
        public ResultData<String> reduce(String accountCode, BigDecimal amount) {
            return ResultData.fail("接口熔断");
        }
    }
    
  2. 编写 FallbackFactory

    @Component
    public class AccountFeignFallbackFactory implements FallbackFactory<AccountFeign> {
        @Override
        public AccountFeign create(Throwable throwable) {
            AccountFeignFallback accountFeignFallback = new AccountFeignFallback();
            accountFeignFallback.setCause(throwable);
            return accountFeignFallback;
        }
    }
    
  3. 给 feign 接口指定熔断工厂

    @FeignClient(name = "account-service",fallbackFactory = AccountFeignFallbackFactory.class)
    public interface AccountFeign {
        ...
    }
    
  4. 在消费中配置文件中开启熔断

    feign:
      sentinel:
        enabled: true
    

经过以上四步我们就可以实现了接口的熔断,接下来重新启动 order-service 验证结果。系统居然无法正常启动!!!

img

堆栈信息如下:

2020-10-23 15:22:14,286 ERROR SpringApplication:826 - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'feignController': Unsatisfied dependency expressed through field 'accountFeign'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.javadaily.feign.account.AccountFeign': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: No fallbackFactory instance of type class com.javadaily.feign.factory.AccountFeignFallbackFactory found for feign client account-service
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:643) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:130) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]

问题解决

俗话说出现问题不可怕,可怕的是我们害怕出现问题,看上面的启动日志应该是无法找到 AccountFeignFallbackFactory,接下来我们看一下 FeignClient 的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
	...省略部分代码,保留关键代码...

	/**
	 * Fallback class for the specified Feign client interface. The fallback class must
	 * implement the interface annotated by this annotation and be a valid spring bean.
	 * @return fallback class for the specified Feign client interface
	 */
	Class<?> fallback() default void.class;
	...省略部分代码,保留关键代码...
}

注意看注释部分,说的是 Feign 的降级类必须实现该 Feign 接口并且必须是一个 Spring Bean。

出现错误的原因应该是降级类没有被注册成 Spring Bean。

大家都知道,在 SpringBoot 启动的时候会默认扫描主启动类所在的包以及子包进行 Bean 实例化。

项目中的 order-service 的主启动类位于 com.javadaily.order,那么 order-service 项目启动时只会扫描到 com.javadaily.order 以及 com.javadaily.order 的子包,而 AccountFeignClient 的降级类 AccountFeignFallback 的包路径为 com.javadaily.feign.fallback ,这样就无法被 Spring 实例化,最终导致项目启动失败。

既然定位到了问题原因那就很好解决了,只要在启动类上配置 Feign 降级类的包路径即可。

@SpringBootApplication(scanBasePackages = {"com.javadaily.feign"})

加上这个配置后系统能正常启动,但是调用接口又返回如下的错误:

{
  "timestamp": "2020-10-27T03:30:44.664+0000",
  "status": 401,
  "error": "Unauthorized",
  "message": "Unauthorized",
  "path": "/order/getAccount/jianzh5"
}

出现这个问题的原因是我们通过 scanBasePackages 配置 Feign 降级类的路径后自身的 Bean 无法实例化,所以我们还需要配置上我们自己项目的扫描路径,即:

@SpringBootApplication(scanBasePackages = {"com.javadaily.feign","com.javadaily.order"})

至此所有问题都解决,我们再次访问接口,当系统故障时返回我们默认结果。

image.png

小结

本章内容我们通过给 Feign 的接口加上熔断器,当实例故障时系统会返回默认数据。这样就不会出现当某一个服务不可用时导致他的消费者长时间等待,线程池耗尽,进而影响到其他服务的线程调用,这也是常说的 "雪崩效应"。

当然了实现过程中出现了一点小挫折,总结下来就是如果各位的 Feign 客户端是由消费者自己编写,位于消费者自己模块不会出现这个问题。如果是由生产者编写并提供则需要注意 Spring Bean 实例化的扫描路径,如果无法扫描实例化熔断类,只需要在启动类上通过 scanBasePackages 扫描到对应的路径即可

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1458 回帖 • 118 关注
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...