学习目标
- 负载均衡Ribbon
- 声明式服务调用Feign
1.负载均衡Ribbon
1.1.什么是负载均衡
通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。
1.2.自定义实现负载均衡
1.2.1.创建服务提供者
1.2.1.1.创建工程
拷贝nacos_provider:
1.2.1.2.application.yml
server:
port: 9090
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.182.132:8848
application:
name: ribbon-provider
server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.182.132:8848
application:
name: ribbon-provider
1.2.2.创建服务消费者
1.2.2.1.创建工程
拷贝nacos_consumer:
1.2.2.2.application.yml
server:
port: 80
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.209.129:8848
application:
name: ribbon-consumer
1.2.2.3.controller
package com.bjpowernode.controller;
import com.bjpowernode.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Random;
/**
* Created with IntelliJ IDEA.
* 随机策略
* @Author: TOM
* @Date: 2022/10/09/17:21
* @Description:
*/
@RestController
@RequestMapping("/consumer")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//springcloud提供的工具类,作用:发现服务
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/getUserById/{id}")
public User getUserById(@PathVariable Integer id){
//1、使用随机策略解决负载均衡的问题
// 随机获取一个随机数,随机数的取值范围是获取服务集合中元素的个数
List<ServiceInstance> instancesList = discoveryClient.getInstances("ribbon-provider");
int currentIndex = new Random().nextInt(instancesList.size());
//获取服务
ServiceInstance service = instancesList.get(currentIndex);
String url = "http://"+ service.getHost() +":"+ service.getPort() +"/provider/getUserById/"+id;
return restTemplate.getForObject(url, User.class);
}
}
1.2.3.测试
使用随机策略进行测试
随机访问provider使用轮询策略测试
1.1.Ribbon介绍
1.1.1.什么是Ribbon
- Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
- 在nacos里面已经集成了ribbon的依赖:
- Ribbon默认提供 很多种负载均衡算法,例如轮询、随机 等等。
1.1.2.负载均衡策略
负载均衡接口:com.netflix.loadbalancer.IRule
1.1.2.1.随机策略
com.netflix.loadbalancer.RandomRule
:该策略实现了从服务清单中随机选择一个服务实例的功能。
1.1.2.2.轮询策略
com.netflix.loadbalancer.RoundRobinRule
:该策略实现按照线性轮询的方式依次选择实例的功能。具体实现如下,在循环中增加了一个count计数变量,该变量会在每次轮询之后累加并求余服务总数
1.3.基于ribbon实现负载均衡
原理:在RestTemplate上添加@LoadBalanced后,ribbon会给RestTemplate的请求添加拦截器,在拦截器中根据serverId从nacos中获得List,然后再使用ribbon的负载均衡算法从List获得一个service,获得service的ip和端口 之后将"http://ribbon-provider/provider/getUserById/"中的ribbon-provider转换成ip和端口。
ribbon经过负载均衡算法,平均分配给你一个服务的ip和端口,之后用restTemplate进行调用
ribbon缺点:拼接url和参数显得好傻、不能剔除死亡节点
1.3.1.修改ribbon_consumer
1.3.1.1.ConfigBean
package com.bjpowernode.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class BeanConfiger {
//RestTemplate:是spring提供的一个工具类,作用是发送restful请求
/**
* 在RestTemplate上添加@LoadBalanced后,ribbon会给RestTemplate的请求添加拦截器
* 在拦截器中根据serverId从nacos中获得List<Service>,然后再使用ribbon的负载均衡算法从 List<Service>获得一个service
* 获得service的ip和端口 之后将"http://ribbon-provider/provider/getUserById/"中的ribbon-provider转换成ip和端口
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
//将负载均衡算法放入容器
@Bean
public IRule iRule(){
return new RandomRule();
}
}
1.3.1.2.controller
package com.bjpowernode.controller;
import com.bjpowernode.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Random;
/**
* Created with IntelliJ IDEA.
*
* @Author: TOM
* @Date: 2022/10/09/17:21
* @Description:
*/
@RestController
@RequestMapping("/consumer")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//springcloud提供的工具类,作用:发现服务
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/getUserById/{id}")
public User getUserById(@PathVariable Integer id){
//将ip和端口改为nacos中的服务名
String url = "http://ribbon-provider/provider/getUserById/"+id;
return restTemplate.getForObject(url, User.class);
}
}
1.3.2.测试
- 分别使用轮询和随机策略调用服务提供者
2.声明式服务调用Feign
2.1.背景
当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率低下,并且显得好傻。
那么有没有更好的解决方案呢?答案是确定的有,Netflix已经为我们提供了一个框架:Feign。
2.2.Feign概述
Feign是Spring Cloud提供的声明式、模板化的HTTP客户端, 它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加一个注解即可。
Spring Cloud集成Feign并对其进行了增强,使Feign支持了Spring MVC注解;Feign默认集成了Ribbon,所以Fegin默认就实现了负载均衡的效果。
说白了Feign就是RestTemplate + Ribbon,声明式是指先将要调用的服务中的controller中的方法声明出来,定义一个接口来声明,之后调用接口中的方法,就像调用本地的方法一样
2.3.Feign入门
2.3.1.创建服务提供者
2.3.1.1.创建工程
- 拷贝ribbon_provider_1
2.3.1.2.application.yml
server:
port: 9092
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.209.129:8848
application:
name: feign-provider
2.3.2.创建feign接口
2.3.2.1.创建工程
2.3.2.2.pom.xml
<?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_parent</artifactId>
<groupId>com.bjpowernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>feign_interface</artifactId>
<dependencies>
<!--Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.bjpowernode</groupId>
<artifactId>springcloud_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
2.3.2.3.feign
package com.bjpowernode.feign;
import com.bjpowernode.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Created with IntelliJ IDEA.
*
* @Author: TOM
* @Date: 2022/10/11/18:38
* @Description:
*/
@FeignClient("feign-provider")
@RequestMapping("/provider")
public interface UserFeign {
@RequestMapping("/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id);
}
2.3.3.创建服务消费者
2.3.3.1.创建工程
- 拷贝ribbon_consumer
2.3.3.2.pom.xml
<?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_parent</artifactId>
<groupId>com.bjpowernode</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ribbon_consumer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.bjpowernode</groupId>
<artifactId>springcloud_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--feign接口-->
<dependency>
<groupId>com.bjpowernode</groupId>
<artifactId>feign_interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
2.3.3.3.application.yml
server:
port: 80
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.182.132:8848
application:
name: feign-consumer
2.3.3.4.Controller
package com.bjpowernode.controller;
import com.bjpowernode.feign.UserFeign;
import com.bjpowernode.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created with IntelliJ IDEA.
*
* @Author: TOM
* @Date: 2022/10/09/17:21
* @Description:
*/
@RestController
@RequestMapping("/consumer")
public class UserController {
@Autowired
private UserFeign userFeign;
@RequestMapping("/getUserById/{id}")
public User getUserById(@PathVariable Integer id){
return userFeign.getUserById(id);
}
}
2.3.3.4.app
package com.bjpowernode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
//@EnableFeignClients(basePackages = "com.bjpowernode.feign")
@EnableFeignClients//开启feign接口扫描
public class FeignConsumerApp {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApp.class,args);
}
}
2.3.4.测试
2.4.Feign原理
2.4.1.FeignInvocationHandler实现 InvocationHandler
通过 JDK Proxy 生成动态代理类,核心步骤就是需要定制一个调用处理器,具体来说,就是实现JDK中位于java.lang.reflect 包中的 InvocationHandler 调用处理器接口,并且实现该接口的 invoke(…) 抽象方法。
为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做 FeignInvocationHandler 类,该类处于 feign-core 核心jar包中。当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成 HystrixInvocationHandler 调用处理器类,类处于 feign-hystrix 的jar包中。
默认的调用处理器 FeignInvocationHandler 是一个相对简单的类,有一个非常重要Map类型成员 dispatch 映射,保存着远程接口方法到MethodHandler方法处理器的映射。
默认的调用处理器 FeignInvocationHandle,在处理远程方法调用的时候,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器,然后交给MethodHandler 完成实际的HTTP请求和结果的处理。前面示例中的 DemoClient 远程调用接口,有两个远程调用方法,所以,其代理实现类的调用处理器 FeignInvocationHandler 的dispatch 成员,有两个有两个Key-Value键值对。
package feign;
//...省略import
public class ReflectiveFeign extends Feign {
//...
//内部类:默认的Feign调用处理器 FeignInvocationHandler
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
//方法实例对象和方法处理器的映射
private final Map<Method, MethodHandler> dispatch;
//构造函数
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
//默认Feign调用的处理
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//...
//首先,根据方法实例,从方法实例对象和方法处理器的映射中,
//取得 方法处理器,然后,调用 方法处理器 的 invoke(...) 方法
return dispatch.get(method).invoke(args);
}
//...
}
源码很简单,重点在于invoke(…)方法,虽然核心代码只有一行,但是其功能是复杂的:
(1)根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器;
(2)调用MethodHandler方法处理器的 invoke(…) 方法,完成实际的HTTP请求和结果的处理。
2.4.2.方法处理器 MethodHandler
Feign的方法处理器 MethodHandler 是一个独立的接口,定义在 InvocationHandlerFactory 接口中,仅仅拥有一个invoke(…)方法,源码如下:
//定义在InvocationHandlerFactory接口中
public interface InvocationHandlerFactory {
//…
//方法处理器接口,仅仅拥有一个invoke(…)方法
interface MethodHandler {
//完成远程URL请求
Object invoke(Object[] argv) throws Throwable;
}
//...
}
MethodHandler 的invoke(…)方法,主要职责是完成实际远程URL请求,然后返回解码后的远程URL的响应结果。Feign提供了默认的 SynchronousMethodHandler 实现类,提供了基本的远程URL的同步请求处理。 SynchronousMethodHandler实现了MethodHandle,调用MethodHandler 的invoke(…)方法实际上是调用 SynchronousMethodHandler的hander方法。该方法会生成url模板创建RequestTemplate(url、requestMethod、body)
public Object invoke(Object[] argv) throws Throwable {
//创建一个RequestTemplate
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
//发出请求
return this.executeAndDecode(template);
} catch (RetryableException var8) {
... ... ...
}
}
}
package feign;
public final class RequestTemplate implements Serializable {
... ... ... ... ... ...
private UriTemplate uriTemplate;
private HttpMethod method;
private Body body;
... ... ... ... ... ...
}
2.4.3.发起请求
SynchronousMethodHandler.executeAndDecode():
通过RequestTemplate生成Request,然后把Request交给Client去处理,Client可以是JDK原生的URLConnection,Apache的HttpClient,也可以时OKhttp,最后Client结合Ribbon负载均衡发起服务调用。
Object executeAndDecode(RequestTemplate template) throws Throwable {
//生成请求对象
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
//发起请求
response = this.client.execute(request, this.options);
} catch (IOException var15) {
... ... ...
throw FeignException.errorExecuting(request, var15);
}
}
总之执行原理大致为
1、扫描feign接口生成代理类并交给spring容器管理
@EnableFeignClients开启feign接口扫描:FeignClientsRegistrar.registerFeignClients()扫描被@FeignClient标识的接口生成代理类,在扫描接口时候解析注解信息获取ip和端口号,将接口中的方法存在dispath映射集合中,之后将代理对象交给spring的容器管理
2、根据接口上的注解创建RequestTemplate
当controller调用feign代理类时,代理对象首先调用 FeignInvocationHandler的invoke方法,获取methodhandler,之后通过反射调用SynchronousMethodHandler.invoke()创建RequestTemplate(url、requestMethod、body)
3、发送请求
接着通过RequestTemplate创建Request,然后client(HttpClient、OkHttp、URLConnection)使用Request发送请求
2.5.Feign参数传递
传参方式:
1、?传参 路径拼接传参
@RequestParam("id")
2、restful传参 路径传参
@PathVariable("id")
3、pojo传参 参数为对象
@RequestBody
测试
1、拼接?号url 使用 @RequestParam
//consumer的controller
@RequestMapping("/delUserById")
public User delUserById(Integer id){
return userFeign.delUserById(id);
}
//feign接口
@RequestMapping("/delUserById")
User delUserById(@RequestParam Integer id);
//provider的controller
@RequestMapping("/delUserById")
public User delUserById(Integer id){
return userService.delUserById(id);
2、传参为pojo
//consumer的controller
@RequestMapping("/addUser")
public User addUser(User user){
return userFeign.addUser(user);
}
//feign接口
@RequestMapping("/addUser")
User addUser(@RequestBody User user);
//provider的controller
@RequestMapping("/addUser")
public User addUser(@RequestBody User user){
return userService.addUser(user);
}
3、传参为数组,实质也是路径拼接传参
//consumer的controller
@RequestMapping("/delUserByIds")
public Integer[] delUserByIds(Integer[] ids){
return userFeign.delUserByIds(ids);
}
//feign接口
@RequestMapping("/delUserByIds")
Integer[] delUserByIds(@RequestParam("ids") Integer[] ids);
//provider的controller
@RequestMapping("/delUserByIds")
public Integer[] delUserByIds(Integer[] ids){
return userService.delUserByIds(ids);
}
4、传参方式为集合,实质也是pojo
//consumer的controller
@RequestMapping("/addUsers")
public List<User> addUsers(){
List<User> userList = new ArrayList<>();
userList.add(new User(1, "ZHAOLIU", 18));
userList.add(new User(1, "LIQI", 17));
userList.add(new User(1, "SONGBBA", 16));
return userFeign.addUsers(userList);
}
//feign接口
@RequestMapping("/addUsers")
List<User> addUsers(@RequestBody List<User> userList);
//provider的controller
@RequestMapping("/addUsers")
public List<User> addUsers(@RequestBody List<User> userList){
return userService.addUsers(userList);
}
2.6.Feign优化
2.6.1.添加feign日志
feign:
client:
config:
default: #这里default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
LoggerLevel: FULL #feign日志
logging:
level:
com.bjpowernode.feign: debug #log4j的日志级别
2.6.2 连接优化
Feign底层的客户端实现:
URLConnection:默认实现,不支持连接池
Apache HttpClient:支持连接池
OKHttp:支持连接池
优化方向:
使用连接池代替默认的URLConncetion
日志级别,最好用baseic或none
添加httpclient依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
切换http客户端成功
2.6.2 通讯优化
2.6.2.1 GZIP简介
gzip介绍:
gzip是一种数据格式,采用用deflate算法压缩数据;gzip是一种流行的数据压缩算法,应用十分广泛,尤其是在Linux平台。
gzip能力:
当Gzip压缩到一个纯文本数据时,效果是非常明显的,大约可以减少70%以上的数据大小。
gzip作用:
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是Gzip与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取gzip文件来比普通手工抓取更快地检索网页。
2.6.2.2 HTTP协议中关于压缩传输的规定(原理)
第一:
客户端向服务器请求头中带有:Accept-Encoding:gzip, deflate 字段,向服务器表示,客户端支持的压缩格式(gzip或者deflate),如果不发送该消息头,服务器是不会压缩的。
第二:
服务端在收到请求之后,如果发现请求头中含有Accept-Encoding字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带Content-Encoding:gzip消息头,表示响应报文是根据该格式压缩过的。
第三:
客户端接收到响应之后,先判断是否有Content-Encoding消息头,如果有,按该格式解压报文。否则按正常报文处理。
2.6.2.3 在Feign技术中应用GZIP压缩
3.在Feign技术中应用GZIP压缩
在Spring Cloud微服务体系中,一次请求的完整流程如下:
在整体流程中,如果使用GZIP压缩来传输数据,涉及到两次请求-应答。而这两次请求-应答的连接点是Application Client,那么我们需要在Application Client中配置开启GZIP压缩,来实现压缩数据传输。
2.6.2.4 只配置Feign请求-应答的GZIP压缩
在交互数据量级不够的时候,看不到压缩内容。
这里只开启Feign请求-应答过程中的GZIP,也就是浏览器-Application Client之间的请求应答不开启GZIP压缩。
在全局配置文件中,使用下述配置来实现Feign请求-应答的GZIP压缩
server:
port: 80
compression:
enabled: true #开启浏览器<----->consumer的gzip压缩
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.182.132:8848 #注册中心的地址
application:
name: feign-consumer
feign:
client:
config:
default: #这里default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
LoggerLevel: full
compression:
request:
enabled: true #开启feign<---->provider的gzip压缩
min-request-size: 2048
response:
enabled: true
logging:
level:
com.bjpowernode.feign: debug #log4j的日志级别
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUser() {
//模拟网络延迟
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new User(1,"王粪堆",18);
}
}
2.6.2.5 超时优化
模拟服务
@Service
public class UserServiceImpl implements UserService {
@Override
public List<User> addUsers(List<User> userList) {
try {
//模拟业务处理时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userList;
}
}
设置超时时间
使用ribbon设置
ribbon:
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 1000 #请求处理的超时时间
使用feign设置
feign:
client:
config:
feign-provider:
ConnectionTimeout: 5000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
测试
不优化超时时间则报错,默认一秒
优化超时时间
feign:
client:
config:
feign-provider:
ConnectionTimeout: 5000 #请求连接的超时时间
ReadTimeout: 5000 #请求处理的超时时间
完美运行!