Ribbon & Feign
第一节 Ribbon
1. Ribbon 介绍
Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP的客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。
2. Ribbon 作用
- 在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。
- 当集群里的1台或者多台服务器down的时候,剩余的没有down的服务器可以保证服务的继续使用。
- 使用了更多的机器保证了机器的良性使用,不会由于某一高峰时刻导致系统cpu急剧上升。
3. Ribbon 与 Eureka 搭配结构图
4. Ribbon 应用
4.1 在所有的qf-user服务中添加服务接口
package com.qf.cloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${server.port}")
private int port;
@GetMapping
public String getUserInfo(){
return "This user comes from " + port;
}
}
4.2 在qf-goods中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
4.3 在qf-goods中编写控制器GoodsController
package com.qf.cloud.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/goods")
public class GoodsController {
@GetMapping
public String buyGoods(){
//购买商品的时候需要获取用户信息
return "";
}
}
4.4 启动类GoodsServer中添加如下代码
@Bean
public RestTemplate restTemplate(){//服务之间用来通信使用的就是这个对象
return new RestTemplate();
}
4.5 完善GoodsController
package com.qf.cloud.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
public String buyGoods(){
//购买商品的时候需要获取用户信息
String userInfo = restTemplate.getForObject("http://localhost:2001/user", String.class);
return userInfo + ", The method name is buyGoods";
}
}
4.6 启动服务,在浏览器中访问
4.7 负载均衡
如果qf-user这样的服务被部署了多份,此时,应该如何来访问呢?
-
RestTemplate开启负载均衡功能
@Bean @LoadBalanced //开启负载均衡 public RestTemplate restTemplate(){//服务之间用来通信使用的就是这个对象 return new RestTemplate(); }
-
使用服务名去获取信息
@GetMapping public String buyGoods(){ //购买商品的时候需要获取用户信息 String userInfo = restTemplate.getForObject("http://USER-SERVICE/user", String.class); return userInfo + " buy goods"; }
4.8 再次测试
第二节 Ribbon 核心组件 IRule
1. 类图
2. 使用说明
- RoundRobinRule–轮询,默认规则
- RandomRule–随机
- AvailabilityFilteringRule --会先过滤掉由于多次访问故障处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对于剩余的服务列表按照轮询的策略进行访问
- WeightedResponseTimeRule–根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越大。刚启动时如果统计信息不足,则使用轮询的策略,等统计信息足够会切换到自身规则
- RetryRule-- 先按照轮询的策略获取服务,如果获取服务失败则在指定的时间内会进行重试,获取可用的服务
- BestAvailableRule --会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量小的服务
- ZoneAvoidanceRule – 复合判断Server所在区域的性能和Server的可用行选择服务器。
3. 启动类中更改负载均衡算法
@Bean
public IRule rule(){
return new RandomRule(); //负载均衡规则:随机
}
启动服务,在浏览器中访问,然后刷新,观察浏览器中内容变化
4 自定义负载均衡算法
4.1 自定义负载均衡规则IpHashRule
package com.qf.cloud.rule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Random;
public class IpHashRule extends AbstractLoadBalancerRule {
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object o) {
ILoadBalancer loadBalancer = this.getLoadBalancer();
if(loadBalancer == null) return null;
List<Server> reachableServers = loadBalancer.getReachableServers();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = sra.getRequest();
String ip = getIpAddress(request);
if(ip == null){
Random r = new Random();
int index = r.nextInt(reachableServers.size());
return reachableServers.get(index);
} else {
int hash = ip.hashCode();
//hash值可能很大 因此需要取模
//比如 hash值为9876 server数量 5 9876 % 5
int index = hash % reachableServers.size();
return reachableServers.get(index);
}
}
/**
* 获取Ip地址
* @return
*/
private String getIpAddress(HttpServletRequest request) {
//请求属性 从请求上下文持有者中获取一个请求属性,因为我们使用的是web请求,因此这个请求属性就是
//一个Servlet请求属性
String Xip = request.getHeader("X-Real-IP");
String XFor = request.getHeader("X-Forwarded-For");
if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = XFor.indexOf(",");
if(index != -1){
return XFor.substring(0,index);
}else{
return XFor;
}
}
XFor = Xip;
if(StringUtils.isNotEmpty(XFor) && !"unKnown".equalsIgnoreCase(XFor)){
return XFor;
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(XFor) || "unknown".equalsIgnoreCase(XFor)) {
XFor = request.getRemoteAddr();
}
return XFor;
}
}
4.2 配置自定义负载均衡规则
package com.qf.cloud.config;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.qf.cloud.rule.IpHashRule;
@Configuration
public class RibbonConfig {
@Bean
public IRule rule(){
return new IpHashRule();
}
}
package com.qf.cloud;
import com.qf.cloud.config.RibbonConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
//Ribbon客户端配置 name表示该客户端需要消费的服务名 configuration表示该客户端的配置
@RibbonClient(name = "user-service", configuration = RibbonConfig.class)
public class GoodsServer {
public static void main(String[] args) {
SpringApplication.run(GoodsServer.class, args);
}
@Bean
@LoadBalanced //开启负载均衡
public RestTemplate restTemplate(){//服务之间用来通信使用的就是这个对象
return new RestTemplate();
}
// @Bean
// public IRule rule(){
// return new RandomRule(); //负载均衡规则:随机
// }
}
启动服务,在浏览器中访问,然后刷新,观察浏览器中内容变化
第三节 Feign
1. Feign 介绍
Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。
2. Feign 的作用
旨在使编写Java Http客户端变得更容易。在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务器依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
3. Feign 应用
3.1 创建工程 qf-goods02
<?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>qf-cloud</artifactId>
<groupId>com.qf</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>qf-goods02</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
3.2 编写启动类
package com.qf.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
//启用Feign客户端
//Feign客户端的感知是通过@SpringBootApplication注解来扫描的,如果扫描不到,那么需要使用@EnableFeignClients
//注解的basePackages属性来指定Feign端所处位置
@EnableFeignClients
public class GoodsServer02 {
public static void main(String[] args) {
SpringApplication.run(GoodsServer02.class, args);
}
}
3.3 编写FeignClient接口
package com.qf.cloud.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
//@FeignClient表示这是一个Feign客户端
//name属性表示该客户端访问的服务名称
//path表示访问的URL地址的前缀,相当于控制器上的RequestMapping中的value属性值
@FeignClient(name = "user-service", path = "/user")
public interface UserClient {
@GetMapping
String getUserInfo();
}
3.4 编写控制器类
package com.qf.cloud.controller;
import com.qf.cloud.client.UserClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private UserClient userClient;
@GetMapping
public String buyGoods(){
//购买商品的时候需要获取用户信息
String userInfo = userClient.getUserInfo();
return userInfo + ", The method name is buyGoods";
}
}
3.5 yml配置
server:
port: 13001 #为了方便管理,服务提供者的端口设定在2000-3000
spring:
application:
name: goods-service #微服务的名称
eureka:
instance:
hostname: localhost
instance-id: goods-service13001 # eureka中显示的微服务的实例名称
client:
service-url:
#查询和注册服务的地址
defaultZone: http://localhost:11000/eureka
启动服务,在浏览器中访问
3.6 FeignClient实现原理
在qf-goods工程中进行模拟
3.6.1 定义FeignClient注解
package com.qf.cloud.client;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
String name() default ""; //Feign客户端需要调用的服务的名称
String path() default ""; //URL地址匹配前缀
}
3.6.2 定义UserClient接口
package com.qf.cloud.client;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "user-service", path="/user")
public interface UserClient {
@GetMapping
String getUserInfo();
}
3.6.3 编写客户端代理对象工具类
package com.qf.cloud.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class FeignClientService {
@Autowired
private RestTemplate restTemplate;
/**
* 获取Feign客户端接口的代理对象,这里的实现方式类似Mybatis中的Mapper接口的代理对象的获取
* @param clazz
* @param <T>
* @return
*/
public <T> T getFeignClient(Class<T> clazz){
if(!clazz.isInterface()) throw new IllegalArgumentException(clazz.getName() + " is not an interface");
FeignClient client = clazz.getAnnotation(FeignClient.class);
if(client == null) throw new IllegalArgumentException( clazz.getName() + "is not a feign client");
ClassLoader loader = clazz.getClassLoader();
Class[] interfaces = { clazz };
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这里只需要两个参数: 一个是url地址, 一个是返回值类型
String path = client.path();
boolean isRequestMethod = false; //是否是匹配请求的方法
GetMapping gm = method.getAnnotation(GetMapping.class);
if(gm != null){
isRequestMethod = true;
String[] value = gm.value();
if(value.length == 1) path += value[0];
else if(value.length > 1) throw new IllegalArgumentException( clazz.getName() + "." + method.getName() + "匹配了多个URL地址");
}
PostMapping pm = method.getAnnotation(PostMapping.class);
if(pm != null){
isRequestMethod = true;
String[] value = gm.value();
if(value.length == 1) path += value[0];
else if(value.length > 1) throw new IllegalArgumentException( clazz.getName() + "." + method.getName() + "匹配了多个URL地址");
}
PutMapping mapping = method.getAnnotation(PutMapping.class);
if(mapping != null){
isRequestMethod = true;
String[] value = gm.value();
if(value.length == 1) path += value[0];
else if(value.length > 1) throw new IllegalArgumentException( clazz.getName() + "." + method.getName() + "匹配了多个URL地址");
}
DeleteMapping dm = method.getAnnotation(DeleteMapping.class);
if(dm != null){
isRequestMethod = true;
String[] value = gm.value();
if(value.length == 1) path += value[0];
else if(value.length > 1) throw new IllegalArgumentException( clazz.getName() + "." + method.getName() + "匹配了多个URL地址");
}
if(!isRequestMethod) throw new IllegalArgumentException(clazz.getName() + "." + method.getName() + "不是处理请求的方法");
String url = String.format("http://%s/%s", client.name(), path);
Class returnType = method.getReturnType();
return restTemplate.getForObject(url, returnType, args);
}
};
return (T) Proxy.newProxyInstance(loader, interfaces, handler);
}
}
3.6.4 GoodsController改造
package com.qf.cloud.controller;
import com.qf.cloud.client.FeignClient;
import com.qf.cloud.client.FeignClientService;
import com.qf.cloud.client.UserClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/goods")
public class GoodsController {
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private FeignClientService feignClientService;
@GetMapping
public String buyGoods(){
//购买商品的时候需要获取用户信息
// String userInfo = restTemplate.getForObject("http://localhost:2001/user", String.class);
UserClient client = feignClientService.getFeignClient(UserClient.class);
String userInfo = client.getUserInfo();
return userInfo + ", The method name is buyGoods";
}
}
3.6.5 GoodsServer改造
package com.qf.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
public class GoodsServer {
public static void main(String[] args) {
SpringApplication.run(GoodsServer.class, args);
}
@Bean
@LoadBalanced //使用服务名称去调用服务时,必须要使用负载均衡
public RestTemplate restTemplate(){
return new RestTemplate();
}
}