这是一个从零开始的springcloud的系列教程,如果你从中间开始看,可能会看不明白.进入我的博客查看其他文章
前言
在之前课程中,我们学会创建了多工程项目,spring-cloud-learn.
搭建的项目有:
- eureka-server:服务治理中心,为微服务提供注册和管理
- eureka-client:在此工程学会了如何向服务端注册,如何发现其他服务,学会如何使用ribbon+restTemplate来访问eureka-client2的服务.
- eureka-client2: 提供了简单的api供eureka-client访问
现在我们来学如何用更加人性化的访问方式来实现微服务之间的调用.学习如何使用熔断器来防止雪崩效应.
在开始前:
- 启动eureka-server微服务
Feign
在之前我们知道ribbo+restTemplate来访问微服务,不过似乎还不够简单,现在我们需要用更简单的方式来完成微服务之间的调用.
Feign简介
Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。简而言之:
Feign 采用的是基于接口的注解
Feign 整合了ribbon,具有负载均衡的能力
整合了Hystrix,具有熔断的能力
打开我们之前创建的工程spring-cloud-learn.沿用我们之前创建的多maven工程.启动eureka-server,eureka-client
创建一个工程.eureka-client-feign.
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>spring-cloud-learn</artifactId>
<groupId>com.jack</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-client-feign</artifactId>
<dependencies>
<!--
一定要写称spring-cloud-starter-netflix-eureka-client
如果写错成spring-cloud-netflix-eureka-client,将无法注册
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入feign-->
<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>
创建EurekaClientFeignApplication文件
package com.jack.feign;
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
public class EurekaClientFeignApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientFeignApplication.class, args );
}
}
application.yml
server:
port: 7773
eureka:
client:
serviceUrl:
defaultZone: http://localhost:7770/eureka/
spring:
application:
name: eureka-client-feign
创建FeignService,提供三个方法,访问的服务名字为eureka-client.
- sayStore方法: 请求参数name,要求的返回值为string类型
- getStores方法: 请求参数length,要求的返回值为store类型.如果我们没给feign配置默认解码器,默认的解码器为SpringDecoder.该解码器如果发现返回的内容是application/json类型,会解析成json对象
- updateStore方法:
- put方法,请求体为store,是一个对象
- 如果没有配置默认的编码器,则为SpringEncoder
- 如果@PutMapping中consumes参数有值,则按照consumes参数格式(比如application/json)来解析,如果没有则默认按application/json.
- 如果请求体是String,则按照text/plain编码
package com.jack.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(name = "eureka-client")
// 可以选择不加@Component, 不过编译器会出现错误警告.因为FeignService是动态注入的,会出现FeignService无法找到的警告.
// 加了@Component,向spring声明下我有一个FeignService的bean
@Component
public interface FeignService {
@GetMapping(value = "/hello_store")
String sayStore(@RequestParam(value = "name") String name);
@GetMapping(value = "/stores")
List<Store> getStores(@RequestParam(value = "length", required = false) Integer length);
/**
* 默认没有注解的参数为请求体,如果再加一个参数,则会报错Method has too many Body parameters
* consumes = "application/json", 表示发送的body内容为json
*/
@PutMapping(value = "/stores/{storeId}")
Store updateStore(@PathVariable("storeId") String id, Store store);
}
接着给eureka-client-feign工程创建一个StoreController, 为了方便浏览器访问,三个方法皆用get请求
package com.jack.feign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class StoreController {
private FeignService feignService;
@Autowired
public void setFeignService(FeignService feignService) {
this.feignService = feignService;
}
@GetMapping("/hello_store")
public Object hello(@RequestParam String name) {
return feignService.sayStore(name);
}
@GetMapping("/stores")
public Object getStores() {
return feignService.getStores(2);
}
@PutMapping("/stores/{storeId}")
public Object updateStore(@PathVariable String storeId) {
List<Store> stores = feignService.getStores(2);
Store store = feignService.updateStore(storeId, stores.get(0));
return store;
}
}
eureka-client-feign三个方法建立完毕,开始给eureka-client创建相应的api
eureka-client服务创建api
给eureka-client创建StoreController.写上对应的三个http请求.请求体为实体类型,默认的解析类型都为application/json.如果有需要变更@PutMapping(value = “/stores/{storeId}”, “consumes”=“xxxx”)
package com.jack.eureka_client;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class StoreController {
@GetMapping(value = "/hello_store")
public Object stores() {
return "hello store!!!";
}
@GetMapping(value = "/stores")
public Object stores(@RequestParam(defaultValue = "5") Integer length) {
List<Map<String, Object>> stores = new ArrayList<>();
for (int i = 0; i < length; i++) {
Map<String, Object> store = new HashMap<>();
store.put("name", "jack" + i);
store.put("date", new Date());
stores.add(store);
}
return stores;
}
@PutMapping(value = "/stores/{storeId}")
public Object updateStore(@PathVariable String storeId,
@RequestBody Map<String, Object> store) {
System.out.println(storeId);
System.out.println(store);
return store;
}
}
到了这一步,启动eureka-client-feign和eureka-client微服务,请求eureka-client-feign微服务的stores api,eureka-client-feign收到请求后通过feign发送给eureka-client
我们可以给feign配置自定的encoder,decoder等
feign:
client:
config:
# 这里是服务名字,如果我们给eureka-client配置,要写eureka-client.如果想给全局配就写default.
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
# 编码器
encoder: com.example.SimpleEncoder
# 解码器
decoder: com.example.SimpleDecoder
# 协议
contract: com.example.SimpleContract
feignName2:
....
Spring-Cloud-Feign提供了默认的配置
-
Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
-
Encoder feignEncoder: SpringEncoder
-
Logger feignLogger: Slf4jLogger
-
Contract feignContract: SpringMvcContract
-
Feign.Builder feignBuilder: HystrixFeign.Builder
-
Client feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used.
没有默认配置的有
- Logger.Level
- Retryer: 超时重试
- ErrorDecoder
- Request.Options
- Collection
- SetterFactory
熔断器
想象一下这种情况,如果一个微服务无法提供内容,这时向这个微服务发送请求的线程就会被阻塞.
假设连接的超时时长为5秒,有大量请求在这五秒内涌入,因为对微服务请求没有结束,无法释放线程资源,这表示着很快机器的线程资源将会被消耗殆尽,一旦消耗殆尽后,又引起本服务请求延迟,其他依赖本服务的服务也会开始发生这样的情况,最终如果多米诺骨牌一般,导致所有微服务崩溃.这成为雪崩效应.
为了解决这个问题,业界提出了断路器模型。
-
http请求->服务A->服务B(不可用)->服务A发现服务B不可用->服务A使用熔断器
-
请求变为http请求->服务A->服务A熔断方法
熔断机制
closed->open:正常情况下熔断器为closed状态,当访问同一个接口次数超过设定阈值并且错误比例超过设置错误阈值时候,就会打开熔断机制,这时候熔断器状态从closed->open。
open->half-open:当服务接口对应的熔断器状态为open状态时候,所有服务调用方调用该服务方法时候都是执行本地降级方法,那么什么时候才会恢复到远程调用那?Hystrix提供了一种测试策略,也就是设置了一个时间窗口,从熔断器状态变为open状态开始的一个时间窗口内,调用该服务接口时候都委托服务降级方法进行执行。如果时间超过了时间窗口,则把熔断状态从open->half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用还是失败,则重新设置熔断器状态为open状态,从新记录时间窗口开始时间。
half-open->closed: 当熔断器状态为half-open,这时候服务调用方调用服务接口时候,就可以发起远程调用而不再使用本地降级接口,如果发起远程调用成功,则重新设置熔断器状态为closed状态。
Feign中已包含了熔断器,需要启动熔断器
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
# 隔离策略 THREAD|SEMAPHORE
# THREAD —— 在固定大小线程池中,以单独线程执行,并发请求数受限于线程池大小。
# SEMAPHORE —— 在调用线程中执行,通过信号量来限制并发量。
strategy: SEMAPHORE
在eureka-client-feign创建FeignServiceFallback类,这个类作用就是当服务B发生异常后,将会会使用fallback里的方法进行代替.该类需要注册到spring中且需要实现FeignService
package com.jack.feign;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
@Component
public class FeignServiceFallback implements FeignService {
@Override
public String sayStore(String name) {
return "sorry i am down! " + name;
}
@Override
public List<Store> getStores(Integer length) {
return Collections.emptyList();
}
@Override
public Store updateStore(String id, Store store) {
store.setName("sorry.I am down!");
return store;
}
}
将FeignServiceFallback添加到FeignService中
package com.jack.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(name = "eureka-client", fallback = FeignServiceFallback.class)
// 不加@Component, 不过编译器会出现错误警告,说找不到FeignService,因为FeignService是动态注入的
@Component
public interface FeignService {
....
关闭eureka-client服务,调用eureka-client-feign的api,localhost:7773/hello_store,熔断器起了作用,直接返回我们定义的内容
如果想要捕捉到错误异常,可以使用FallbackFactory.创建文件FeignServiceFallbackFactory
package com.jack.feign;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
@Component
public class FeignServiceFallbackFactory implements FallbackFactory<FeignService> {
@Override
public FeignService create(Throwable throwable) {
return new FeignService() {
@Override
public String sayStore(String name) {
return "sorry: " + throwable.getMessage();
}
@Override
public List<Store> getStores(Integer length) {
System.out.println(throwable);
return Collections.emptyList();
}
@Override
public Store updateStore(String id, Store store) {
store.setName("sorry: " + throwable.getMessage());
return store;
}
};
}
}
修改FeignService文件
package com.jack.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// 改为fallbackFactory,之前是fallback参数
@FeignClient(name = "eureka-client", fallbackFactory = FeignServiceFallbackFactory.class)
// 不加@Component, 不过编译器会出现错误警告,说找不到FeignService,因为FeignService是动态注入的
@Component
public interface FeignService {
....
调用eureka-client-feign的api,localhost:7773/hello_store,打印出错误异常.
总结
我们学会如何使用feign来执行服务之间rpc的调用,并且还学会了使用hystrix来防止服务器的雪崩效应