详细谈谈SpringCloud的负载均衡实现与@LoadBalanced注解和@Qualifier注解

在本文的第一章当中,我们将会大概去介绍SpringCloud如何去实现负载均衡?

在本文的第二章当中,我们将会去介绍@LoadBalanced注解的真正的生效的原理是什么?我们也将详细介绍@Qualifier注解的实现原理。

1. SpringCloud负载均衡与@LoadBalanced注解

也许很多人刚学SpringCloud的时候,跟着视频进行学习时,老师会说@LoadBalanced注解标在RestTemplate上就能实现负载均衡,至于为什么呢?本文的第二章将会进行介绍。

如下面的代码所示(代码为Kotlin代码)

    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

我们打开@LoadBalanced的源码

image.png

我们可以看到@LoadBalanced的源码上标注了一个@Qualifier注解。并且对这个注解的描述信息为,它标识RestTemplateWebClient的Bean,被配置成为了一个LoadBalancerClient

1.1 LoadBalancerClient是什么?

LoadBalancerClient是SpringCloud当中对于负载均衡的客户端的一层抽象,它的实现类有两个RibbonLoadBalancerClientBlockingLoadBalancerClient

image.png

首先我们应该知道的是LoadBalancerClient是来自于spring-cloud-common包下,而
RibbonLoadBalancerClient是来自于spring-cloud-ribbon包下,BlockingLoadBalancerClient是来自于SpringCloud自家做的spring-cloud-loadbalancer包下。

也就是说,SpringCloud当中提供负载均衡的统一接口是LoadBalancerClient,而RibbonLoadBalancer则需要针对于SpringCloud当中提供的规范去进行提供自己的实现。

我们以Ribbon为例

image.png

我们可以看到,RibbonLoadBalancerClient当中execute的实现方式为,首先要获取一个ILoadBalancer,接着根据ILoadBalancer去使用特定的负载均衡策略,选择出来一个合适的Server,来完成本次请求的执行。

image.png

上面我们展示了chooseServer的逻辑,我们发现,它是组合了Rule(规则),来完成的Server的选择。具体的Rule的实现如下,如果你了解过Ribbon,肯定对这些策略并不陌生(起码也会有一定的了解,从我第一次看SpringCloud时,就听说过这个概念)。

image.png

下面是ILoadBalancer的接口规范:

image.png

需要注意的是,不管是ILoadBalancer还是IRule,这些组件所在的包,可都是com.netflix,与SpringCloud可以说是毫无关系。而RibbonLoadBalancerClient是什么?它就是针对SpringCloud的规范,将Netflix的负载均衡策略,全部桥接到SpringCloud当中,让SpringCloud可以使用到Ribbon的负载均衡算法。

BlockingLoadBalancerClient的实现肯定也类似,只不过它肯定用到的是SpringCloudLoadBalancer的LoadBalancer罢了,我们这里就不进行展开讲了,感兴趣的小伙伴可以自行查阅相关的源码。

1.2 为什么加上@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?

是不是觉得很神奇呢?从注解上似乎并未告诉我们为什么以及怎么实现,只是说这是一个标识负载均衡的注解罢了。我们来找到SpringCloudCommon包的源码,并找到自动配置类LoadBalancerAutoConfiguration

image.png

首先,我们可以看到,它直接Autowired注入了容器当中配置的所有的RestTemplate。还给容器中放入了一个SmartInitializingSingleton这样的一个Bean。

在了解后续之前,我们先来了解SmartInitializingSingletonRestTemplateCustomizer是什么?

SmartInitializingSingleton是什么?

相信各位熟悉Spring的朋友,应该都有用过Spring的InitializingBean吧,是一个Bean的初始化方法的回调方法。

image.png

但是也许你会遇到,你想在这里去容器当中去进行各种的getBean的情况。但是实际上在这里完成初始化是有可能产生问题的,因为获取的时机还比较早,这时候Spring容器的有些配置有可能还没完成呢,这时就去getBean就有可能产生一些问题(其实一般情况也遇不到)。

与该接口对应的,Spring还有一个接口是SmartInitializingSingleton,它其实也是作为一个Bean的初始化回调方法,它会在Spring当中的所有的Bean都完成实例化和初始化之后再去进行回调。

image.png

它的具体回调时机如下,第一部分是实例化所有的单实例Bean,第二部分则是回调所有的SmartInitializingSingleton,它也完全可以用来初始化Bean,是没有任何问题的。

image.png

RestTemplateCustomizer是什么?

image.png

首先,我们知道RestTemplate,是一个HttpClient的客户端,可以用于完成HTTP请求的发送和处理,并且它还是一个InterceptingHttpAccessor,支持去添加请求的拦截器,对请求去进行处理。(注意RestTemplate是来自于Spring的web包)

SpringCloud当中,针对于RestTemplate去提供了RestTemplateCustomizer,支持去对RestTemplate去进行自定义吗,我们可以实现这个接口,在customize方法当中去添加,我们自定义的逻辑。

Spring当中,针对于XXXCustomizer的实现,其实非常非常多,我们从名字也可以知道,是对XXX的一个自定义化器,支持去对XXX去进行自定义操作。

image.png

在了解了上面的知识之后,我们继续来看。

image.png

我们已经知道了Spring容器启动过程当中,它会回调所有的RestTemplateCustomizer,那么这些组件从哪来?我们往下翻,就会发现,它给容器当中配置了LoadBalancerInterceptor

image.png
image.png

我们来看LoadBalancerInterceptor的实现:

image.png

我们可以看到,它从request当中获取到uri,并且以uri作为serviceName,把它交给了LoadBalancerClient去进行执行。然后,不就到了刚刚我们说的SpringCloud抽象了LoadBalancerClient了吗?也就是说SpringCloud使用自定义的LoadBalancerInterceptor去拦截了RestTemplate,将请求转交给了LoadBalancerClient去进行处理。

如果我们引入了Ribbon的jar包,那么这个LoadBalancerClient就会是Ribbon的实现;如果我们引入了SpringCloudLoadBalancer的jar包,那么这个LoadBalancerClient就会是SpringCloudLoadBalancer,只要导入一方的相关的实现配置,它就会被SpringBoot所整合,这也就是我们所说的SpringBoot的自动配置。

整体的流程大概如下:

image.png

1.3 自定义RPC远程调用的协议

类似地,如果我们想要自定义相关的负载均衡的功能(并且不想使用SpringCloud提供的RestTemplate或者是SpringCloudOpenFeign)。比如说,我们想不使用HTTP请求去完成RPC的远程调用(RestTemplate/SpringCloudOpenFeign/WebClient都是基于HTTP的),我们只需要从Spring容器当中去注入LoadBalancerClient,并且自定义LoadBalancerRequest回调函数去指定处理请求的逻辑即可实现负载均衡。

image.png

至于在回调函数当中,我们想要使用何种方式去发送网络请求获取数据,那就是我们完全可以去自定义的部分了。

2. @LoadBalanced@Qualifier之间的关系?

在上面的代码当中,我们会见到下面这样的代码,在@Autowired上打了@LoadBalanced注解。

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

这是什么操作?字段上标注@LoadBalanced?干啥的?我这时候有以下两个想法:

  • 1.SpringCloud单独使用BeanPostProcessor单独处理了这个字段?
  • 2.难道Spring还支持Autowired时,还支持打上个注解,去进行注解的匹配吗?

按照我对Spring的源码的理解,使用第一种的概率比较大,于是我找遍了整个SpringCloud的Common包下的源码,没有发现。但是第二种,完全没听说过这种用法,应该是不太可能,我看过的源码里似乎也没有这个插曲。

但是这时候,我想起来了Qualifier注解,这个注解引起了我的单独注意。

2.1 AutowireCandidateResolver是什么

Spring在处理自动注入(不只是针对@Autowired注解,也针对@Resource注解,以及@Inject等注解)时,会使用DefaultListableBeanFactory.resolveDepenency方法去完成依赖的注入。

image.png

在处理自动注入时,会用到AutowireCandidateResolver,去判断容器当中的所有的Bean,挨个去匹配是否可以作为当前依赖的注入对象。从名字,我们也可以知道些什么,AutowireCandidateResolver,自动装配的候选的解析器。至于如何判断?具体逻辑就在isAutowireCandidate方法当中了。

我们找到它的子类QualifierAnnotationAutowireCandidateResolver,这个类是我们需要重点研究的类。

image.png

这里它会从字段上、方法参数上、方法上、构造器上,去匹配Qualifier注解,需要注意的是,这里(以及后续)提到的Qualifier注解,并不只是Spring家的Qualifier,也包含javax.inject当中的Qualifier,甚至是自定义的Qualifier。

有一个点是我们要关注的,向下传递的参数descriptor.getAnnotations()是要进行注入的元素的注解列表。

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

比如上面的代码当中,获取到的注解,就是AutowiredLoadBalanced这两个注解。

image.png

接着,就是要注入的元素上的注解,和Qualfier去进行匹配,如果匹配了(也就是说要注入的元素上有@Qualifier注解的话),那么走进去checkQualifier方法,检查Qualifier注解的情况。这个方法很长,我们分成两段去进行介绍:

image.png

它会从各个地方去检查Qualifier注解,我们主要关注下面三种情况:

    1. bd.getQualifier从BeanDefinition当中去获取,这个主要是XML当中配置的Qualfier属性,就会解析封装到里面。
    1. getQualifiedElementAnnotation,这种情况主要是扫描的时候,是通过注解的方式扫描Class(比如@ComponentScan扫描到了某个类)进来的那种情况。
    1. 如果还找不到?getFactoryMethodAnnotation,从FactoryMethod(工厂方法,也就是@Bean方法)上找Qualifier。

如果找到了?就会在下面触发这样的代码:

targetAnnotation != null && targetAnnotation.equals(annotation)

也就是Bean(类上,@Bean方法上,或者更多地方)上的Qualifier,和要注入的Qualfier完全匹配的话(也就是说是一对一模一样的注解),那么return true。。如果这里没有检查到?那么就是下一段代码了

image.png

这里,它会从annotation(要注入的元素上的Qualifier注解)上去获取到value属性,和bdHolder当中的name是否匹配?如果匹配的话,那么return true。也就是我们常说的,Qualifier去指定beanName去进行注入的情况。

这里其实有一个点,我们值得注意,那就是,如果没有Qualifier注解,那么是根本不会去进行匹配Qualifier注解的,只需要类型匹配,就return true(匹配类型这部分代码在QualifierAnnotationAutowireCandidateResolver的父类当中)。如果有Qualifier注解的话,那么去匹配Qualifier注解的情况。

2.2 我们继续谈谈为什么@LoadBalanced能实现负载均衡?

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

上面这段代码,经过我们上面的分析,它会去匹配RestTemplate上是否有Qualifier注解?并且如果有Qualifier注解的话,必须完全匹配才行。

    @Bean
    @LoadBalanced
    fun restTemplate(): RestTemplate {
        return RestTemplate()
    }

而我们通过上面的代码,因为两边都加了@LoadBalance注解的组合,因此两边都有@Qualifier注解的组合注解,因此能命中下面的代码。

targetAnnotation != null && targetAnnotation.equals(annotation)

那么,如果我们给@Bean方法上打个@Qualifier注解,是不是就行了?

其实不行,在源码当中的判断是,如果@LoadBalanced@Qualifier作为元注解的话,必须匹配根级别注解是否完全一致,也就是匹配两边的@LoadBalanced注解是否完全一样,不只是单独匹配@Qualifier注解

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

我们简单做个分析:SpringCloud会自动注入容器当中的所有的@LoadBalanced的RestTemplate,并使用RestTemplateCustomizer,去给RestTemplate添加LoadBalancerInterceptor,让这个拦截器,将你真正的HTTP请求,直接转交给SpringCloud的LoadBalanceClient,在交给负载均衡组件,比如Ribbon或者是SpringCloudLoadBalancer使用负载均衡策略,选择出来一个合适的实例(Server/ServcieInstance),去完成本次请求的处理工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,585评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,283评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,760评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,461评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,280评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,268评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,656评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,322评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,629评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,691评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,445评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,299评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,694评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,982评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,244评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,642评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,829评论 2 335

推荐阅读更多精彩内容