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超时时间:
- 方法一:通过创建配置类【可一次性配置用于全局】
- 方法二:通过调整配置文件 .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日志:
- 方法一:通过创建配置类【可一次性配置用于全局】
- 方法二:通过调整配置文件 .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模块:
- 添加Feign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 开启feign注解 @EnableFeignClients
@EnableFeignClients // 开启feign注解
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.lihao.mapper")
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class, args);
}
}
- 创建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);
}
- 通过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