一、前言
提到微服务,就不得不说服务间的通信。
服务间的通信常见有4种方式。最基础的是restTemplate,是一种同步的服务间调用;其次是webclient,这是一个异步服务间调用的库;成框架的库,常用的有openfeign和dubbo。
本期介绍这几种方式的简单使用,并对比他们的优劣。
二、restTemplate
restTemplate事Spring框架自带的一种同步调用的库,在早年十分流行。
2.1 pom引用
在芝法酱躺平攻略(1)的framework-common,和framework-web-common我们其实已经加入了相关引用,这里展示相关的引用
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring5</artifactId>
</dependency>
<!-- ******************httpclient****************************-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
2.2 RestTemplateConfig
为了不在每次使用时都写一遍初始化,我们可以创建一个RestTemplate的bean,注入到service中使用。
但由于这种方式并不是现在流行的方式,故此处只做简单的配置
@Configuration
public class RestTemplateConfig {
@Bean(name = "appRestClient")
public RestTemplate getRestClient(FastJsonHttpMessageConverter pFastJsonHttpMessageConverter) {
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder()
.messageConverters(pFastJsonHttpMessageConverter)
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(5));
return restTemplateBuilder.build();
}
}
注意FastJsonHttpMessageConverter pFastJsonHttpMessageConverter在芝法酱躺平攻略(1)中配置过相关的bean。
2.3 使用
建一个模块test,建一个模块tools。在tools中添加一个MathApi的controller,在test模块添加一个TestApi的controller
MathApi
@Api(tags = "test-测试接口")
@RequestMapping("/api/math")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class MathApi {
BigInteger getFibonacii(int n){
BigInteger n1 = new BigInteger("1");
BigInteger n2 = new BigInteger("1");
if(n==1){
return n1;
}else if(n==2){
return n2;
}else {
BigInteger res = new BigInteger("1");
for(int i=3;i<=n;i++){
res = n1.add(n2);
n1 = n2;
n2 = res;
}
return res;
}
}
@Operation(summary = "斐波那契数列")
@GetMapping("/fibonacii/{n}")
BigInteger fibonacii(@PathVariable(name = "n") int n){
return getFibonacii(n);
}
@Operation(summary = "a+b")
@GetMapping("/a_plus_b")
Double aPlusB(@RequestParam(name = "a") double a,
@RequestParam(name = "b") double b){
return a+b;
}
}
TestApi
@Api(tags = "test-测试接口")
@RequestMapping("/api/test")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class TestApi {
private final AppProperty mAppProperty;
private final RestTemplate mRestTemplate;
@Operation(summary = "hello测试")
@GetMapping("/hello")
public String sayHello(){
return mAppProperty.getMyName();
}
@Operation(summary = "斐波那契数列测试")
@GetMapping("/fibonacii/{n}")
RestResponse<BigInteger> fibonacii(@Parameter(description = "n") @PathVariable(name = "n") int n){
RestResponse<BigInteger> rtn = mRestTemplate.getForObject("http://localhost:8010/api/math/fibonacii/{n}",RestResponse.class,n);
return rtn;
}
@Operation(summary = "a+b测试")
@GetMapping("/a_plus_b")
RestResponse<Double> aPlusB(@Parameter(description = "a") @RequestParam(name = "a") double a,
@Parameter(description = "b") @RequestParam(name = "b") double b){
RestResponse<Double> rtn = mRestTemplate.getForObject("http://localhost:8010/api/math/a_plus_b?a={a}&b={b}",RestResponse.class,a,b);
return rtn;
}
}
三、 OpenFeign
openFeign是微服务框架下一个非常常用的库,可以和nacos结合,通过服务名调用对应接口,并附带负载均衡熔断降级的功能。
3.1 pom引用
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2 openfeign接口
package indi.zhifa.recipe.bailan5.busy.feign;
@FeignClient(value = "tools",fallback = MathFeignHystrix.class)
public interface IMathFeignCaller {
@GetMapping("api/math/fibonacii/{n}")
RestResponse<BigInteger> fibonacii(@PathVariable(name = "n") int n);
@GetMapping("api/math/a_plus_b")
RestResponse<Double> aPlusB(@RequestParam(name = "a") double a, @RequestParam(name = "b") double b);
@GetMapping("api/math/timeoutTest/{t}")
RestResponse<String> timeoutTest(@PathVariable(name = "t") int t);
}
@Component
public class MathFeignHystrix implements IMathFeignCaller{
@Override
public RestResponse<BigInteger> fibonacii(int n) {
return RestResponse.error("服务器正忙");
}
@Override
public RestResponse<Double> aPlusB(double a, double b) {
return RestResponse.error("服务器正忙");
}
@Override
public RestResponse<String> timeoutTest(int t) {
return RestResponse.error("服务器正忙");
}
}
3.3 TestApi
@Api(tags = "test-测试接口")
@RequestMapping("/api/test")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class TestApi {
private final AppProperty mAppProperty;
private final IMathFeignCaller mIMathFeignCaller;
@Operation(summary = "hello测试")
@GetMapping("/hello")
public String sayHello(){
return mAppProperty.getMyName();
}
@Operation(summary = "斐波那契数列测试")
@GetMapping("/fibonacii/{n}")
RestResponse<BigInteger> fibonacii(@Parameter(description = "n") @PathVariable(name = "n") int n){
RestResponse<BigInteger> rtn = mIMathFeignCaller.fibonacii(n);
return rtn;
}
@Operation(summary = "a+b测试")
@GetMapping("/a_plus_b")
RestResponse<Double> aPlusB(@Parameter(description = "a") @RequestParam(name = "a") double a,
@Parameter(description = "b") @RequestParam(name = "b") double b){
RestResponse<Double> rtn = mIMathFeignCaller.aPlusB(a,b);
return rtn;
}
@Operation(summary = "超时测试")
@GetMapping("/timeOutTest/{t}")
RestResponse<String> timeOutTest(@Parameter(description = "t") @PathVariable(name = "t") int t){
RestResponse<String> rtn = mIMathFeignCaller.timeoutTest(t);
return rtn;
}
}
3.4 MathApi
@Api(tags = "test-测试接口")
@RequestMapping("/api/math")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class MathApi {
BigInteger getFibonacii(int n){
BigInteger n1 = new BigInteger("1");
BigInteger n2 = new BigInteger("1");
if(n==1){
return n1;
}else if(n==2){
return n2;
}else {
BigInteger res = new BigInteger("1");
for(int i=3;i<=n;i++){
res = n1.add(n2);
n1 = n2;
n2 = res;
}
return res;
}
}
@Operation(summary = "斐波那契数列")
@GetMapping("/fibonacii/{n}")
BigInteger fibonacii(@PathVariable(name = "n") int n){
return getFibonacii(n);
}
@Operation(summary = "a+b")
@GetMapping("/a_plus_b")
Double aPlusB(@RequestParam(name = "a") double a,
@RequestParam(name = "b") double b){
return a+b;
}
@Operation(summary = "超时测试")
@GetMapping("/timeout/{t}")
RestResponse<String> timeoutTest(@PathVariable(name = "t") int t) throws InterruptedException {
Thread.sleep(t);
return RestResponse.ok("睡了"+t/1000.0+"s,我睡醒了");
}
}
四、 熔断降级-resilience4j
4.1 熔断降级的需求
在⾼并发访问下,⽐如天猫双11,流量持续不断的涌⼊,服务之间的相互调⽤频率突然增加,引发系统负载过⾼,这时系统所依赖的服务的稳定性对系统的影响⾮常⼤,⽽且还有很多不确定因素引起雪崩,如⽹络连接中断,服务宕机等。⼀般微服务容错组件提供了限流、隔离、降级、熔断等⼿段,可以有效保护我们的微服务系统。
相关内容可以参考这篇博客。
4.2 熔断器配置
配置属性 | 默认值 | 描述 |
---|---|---|
failureRateThreshold | 50 | 以百分⽐配置失败率阈值。当失败率等于或⼤于阈值时,断路器状态并关闭变为开启,并进⾏服务降级。 |
slowCallRateThreshold | 100 | 以百分⽐的⽅式配置,断路器把调⽤时间⼤于slowCallDurationThreshold的调⽤视为慢调⽤,当慢调⽤⽐例⼤于等于阈值时,断路器开启,并进⾏服务降级。 |
slowCallDurationThreshold | 60000[ms] | 配置调⽤时间的阈值,⾼于该阈值的呼叫视为慢调⽤,并增加慢调⽤⽐例。 |
permittedNumberOfCallsInHalfOpenState | 10 | 断路器在半开状态下允许通过的调⽤次数。 |
maxWaitDurationInHalfOpenState | 0 | 断路器在半开状态下的最⻓等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会⼀直处于半开状态,直到所有允许通过的访问结束。 |
slidingWindowType | COUNT_BASED | 配置滑动窗⼝的类型,当断路器关闭时,将调⽤的结果记录在滑动窗⼝中。滑动窗⼝的类型可以是count-based或time-based。如果滑动窗⼝类型是COUNT_BASED,将会统计记录最近slidingWindowSize次调⽤的结果。如果是TIME_BASED,将会统计记录最近 slidingWindowSize秒的调⽤结果。 |
slidingWindowSize | 100 | 配置滑动窗⼝的⼤⼩。 |
minimumNumberOfCalls | 100 | 断路器计算失败率或慢调⽤率之前所需的最⼩调⽤数(每个滑动窗⼝周期)。例如,如果minimumNumberOfCalls为10,则必须⾄少记录10个调⽤,然后才能计算13失败率。如果只记录了9次调⽤,即使所有9次调⽤都失败,断路器也不会开启。 |
waitDurationInOpenState | 60000 [ms] | 断路器从开启过渡到半开应等待的时间。 |
automaticTransition FromOpenToHalfOpenEnabled | false | 如果设置为true,则意味着断路器将⾃动从开启状态过渡到半开状态,并且不需要调⽤来触发转换。创建⼀个线程来监视断路器的所有实例,以便在WaitDurationInOpenstate之后将它们转换为半开状态。但是,如果设置为false,则只有在发出调⽤时才会转换到半开,即使 在waitDurationInOpenState之后也是如此。这⾥的优点是没有线程监视所有断路器的状态。 |
recordExceptions | empty | 记录为失败并因此增加失败率的异常列表。除⾮通过ignoreExceptions显式忽略,否则与列表中某个匹配或继承的异常都将被视为失败。 如果指定异常列表,则所有其他异常均视为成功,除⾮它们被ignoreExceptions显式忽略。 |
ignoreExceptions | empty | 被忽略且既不算失败也不算成功的异常列表。任何与列表之⼀匹配或继承的异常都不会被视为失败或成功,即使异常是 recordExceptions的⼀部分。 |
recordException | throwable -> true | By default all exceptions are recored as failures. ⼀个⾃定义断⾔,⽤于评估异常是否应记录为失败。 如果异常应计为失败,则断⾔必须返回true。如果出断⾔返回false,应算作成功,除⾮ignoreExceptions显式忽略异常。 |
ignoreException | throwable -> false | By default no exceptions is ignored. ⾃定义断⾔来判断⼀个异常是否应该被忽略异常,则谓词必须返回 true。如果异常应算作失败,则断⾔必须返回 false |
4.3 yml配置
为了测试断路器,把tools的阈值设低些
spring:
datasource:
......
redis:
......
cloud:
circuitbreaker:
resilience4j:
enabled: true
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
resilience4j:
circuitbreaker:
configs:
default:
#以百分⽐配置失败率阈值。当失败率等于或⼤于阈值时,断路器状态并关闭变为开启,并进⾏服务降级。
failureRateThreshold: 50
#以百分⽐的⽅式配置,断路器把调⽤时间⼤于slowCallDurationThreshold的调⽤视为慢调⽤,当慢调⽤⽐例⼤于等于阈值时,断路器开启,并进⾏服务降级。
slowCallRateThreshold: 90
#配置调⽤时间的阈值,⾼于该阈值的呼叫视为慢调⽤,并增加慢调⽤⽐例。
slowCallDurationThreshold: 100000
#断路器在半开状态下允许通过的调⽤次数。
permittedNumberOfCallsInHalfOpenState: 5
#断路器在半开状态下的最⻓等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会⼀直处于半开状态,直到所有允许通过的访问结束。
maxWaitDurationInHalfOpenState: 600000
#配置滑动窗⼝的类型,当断路器关闭时,将调⽤的结果记录在滑动窗⼝中。滑动窗⼝的类型可以是count-based或time-based。如果滑动窗⼝类型是COUNT_BASED,将会统计记录最近slidingWindowSize次调⽤的结果。如果是TIME_BASED,将会统计记录最近 slidingWindowSize秒的调⽤结果。
slidingWindowType: COUNT_BASED
#配置滑动窗⼝的⼤⼩。
slidingWindowSize: 100
#断路器计算失败率或慢调⽤率之前所需的最⼩调⽤数(每个滑动窗⼝周期)。例如,如果minimumNumberOfCalls为10,则必须⾄少记录10个调⽤,然后才能计算失败率。如果只记录了9次调⽤,即使所有9次调⽤都失败,断路器也不会开启。
minimumNumberOfCalls: 5
#断路器从开启过渡到半开应等待的时间。
waitDurationInOpenState: 60000
#不开启监视线程
automaticTransitionFromOpenToHalfOpenEnabled: false
instances:
tools:
registerHealthIndicator: true
baseConfig: default
minimumNumberOfCalls: 3
slowCallDurationThreshold: 2000
permittedNumberOfCallsInHalfOpenState: 1
#超时配置
timelimiter:
configs:
default:
#超时时间
timeoutDuration: 10000
instances:
tools:
timeoutDuration: 2s
swagger:
......
enum-memo:
enum-packages:
- indi.zhifa.recipe.bailan5.busy.entity.enums
mybatis-plus:
type-enums-package: indi.zhifa.recipe.bailan.**.entity.enums
redis:
cache-expire: 120
4.4 pom引用
由于之前已经加入了spring-cloud-gatway的版本管理spring-cloud-dependencies,不需要再写版本号
加入如下依赖即可
<!--熔断降级-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
4.4 修改IMathFeignCaller
resilience4j与feign配合的方式,是在feign接口上另加注解
@FeignClient(value = "tools",fallback = MathFeignHystrix.class)
@CircuitBreaker(name = "tools")
public interface IMathFeignCaller {
@GetMapping("api/math/fibonacii/{n}")
RestResponse<BigInteger> fibonacii(@PathVariable(name = "n") int n);
@GetMapping("api/math/a_plus_b")
RestResponse<Double> aPlusB(@RequestParam(name = "a") double a, @RequestParam(name = "b") double b);
@GetMapping("api/math/timeout/{t}")
RestResponse<String> timeoutTest(@PathVariable(name = "t") int t);
}
4.5 修改异常处理
@ResponseBody
@ExceptionHandler(CallNotPermittedException.class)
public RestResponse callNotPermittedExceptionHandle(Exception e) {
log.info("触发断路器", e);
return RestResponse.error("触发断路器 "+e.toString());
}
4.6 测试
使用浏览器访问http://localhost:8001/api/test/timeout/3000,然后多刷新几次,可以看到如下信息
五、关于dubbo
从官网上看,Spring Cloud Dubbo 从 2021.0.1.0 起已被移除出主干,不再随主干演进,故暂时不再研究该技术。