第四章 微服务容错Resilience4j

1,637 阅读19分钟

4.1 微服务容错简介

\

在高并发访问下,比如天猫双11,流量持续不断的涌入,服务之间的相互调用频率突然增加,引发系统负载过高,这时系统所依赖的服务的稳定性对系统的影响非常大,而且还有很多不确定因素引起雪崩,如网络连接中断,服务宕机等。一般微服务容错组件提供了限流、隔离、降级、熔断等手段,可以有效保护我们的微服务系统。

4.1.1 隔离

\

微服务系统A调用B,而B调用C,这时如果C出现故障,则此时调用B的大量线程资源阻塞,慢慢的B的线程数量持续增加直到CPU耗尽到100%,整体微服务不可用,这时就需要对不可用的服务进行隔离。服务调用关系如图4-1所示。

图4-1 服务调用

\

  1. 线程池隔离

线程池隔离就是通过Java的线程池进行隔离,B服务调用C服务给予固定的线程数量比如12个线程,如果此时C服务宕机了就算大量的请求过来,调用C服务的接口只会占用12个线程不会占用其他工作线程资源,因此B服务就不会出现级联故障。线程池隔离原理,如图4-2所示。

图4-2 线程池隔离

  1. 信号量隔离

隔离信号量隔离是使用Semaphore来实现的,当拿不到信号量的时候直接拒接因此不会出现超时占用其他工作线程的情况。代码如下。

Semaphore semaphore = new Semaphore(10,true);  
//获取信号量  
semaphore.acquire();  
//do something here  
//释放信号量  
semaphore.release();  
  1. 线程池隔离和信号量隔离的区别

线程池隔离针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败。线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。而信号量隔离非常轻量级,仅限制对某个资源调用的并发数,而不是显式地去创建线程池,所以 overhead 比较小,但是效果不错,也支持超时失败。二者区别如表4-1所示。

类别线程池隔离信号量隔离
线程与调用线程不同,使用的是线程池创建的线程与调用线程相同
开销排队,切换,调度等开销无线程切换性能更高
是否支持异步支持不支持
是否支持超时支持超时支持超时
并发支持支持通过线程池大小控制支持通过最大信号量控制

表4-1 线程池隔离和信号量隔离的区别

4.1.2 熔断

\

当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。熔断器模型,如图4-3所示。

图4-3 熔断机模型

熔断器模型的状态机有3个状态。

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。
  • Open:打开状态(断路器打开),所有请求都会被降级。熔断器会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。
  • Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进行休眠计时。

4.1.3 降级

\

降级是指当自身服务压力增大时,系统将某些不重要的业务或接口的功能降低,可以只提供部分功能,也可以完全停止所有不重要的功能。比如,下线非核心服务以保证核心服务的稳定、降低实时性、降低数据一致性,降级的思想是丢车保帅。

举个例子,比如,目前很多人想要下订单,但是我的服务器除了处理下订单业务之外,还有一些其他的服务在运行,比如,搜索、定时任务、支付、商品详情、日志等等服务。然而这些不重要的服务占用了JVM的不少内存和CPU资源,为了应对很多人要下订单的需求,设计了一个动态开关,把这些不重要的服务直接在最外层拒绝掉。这样就有跟多的资源来处理下订单服务(下订单速度更快了)

4.1.4 限流

\

限流,就是限制最大流量。系统能提供的最大并发有限,同时来的请求又太多,比如商城秒杀业务,瞬时大量请求涌入,服务器服务不过来,就只好排队限流了,就跟去景点排队买票和去银行办理业务排队等号道理相同。下面介绍下四种常见的限流算法。

  1. 漏桶算法

漏桶算法的思路,一个固定容量的漏桶,按照常量固定速率流出水滴。如果桶是空的,则不需流出水滴。可以以任意速率流入水滴到漏桶。如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。漏桶限流原理,如图4-4所示。

图4-4 漏桶算法

  1. 令牌桶算法

令牌桶算法:假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌。桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。令牌桶限流原理,如图4-5所示。

图4-5 令牌桶算法

令牌桶限流服务器端可以根据实际服务性能和时间段改变改变生成令牌的速度和水桶的容量。 一旦需要提高速率,则按需提高放入桶中的令牌的速率。

生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味着当面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大。

  1. 固定时间窗口算法

这种实现计数器限流方式由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的,但是将间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮。固定时间窗口算法原理,如图4-6所示。

图4-6 固定时间窗口算法

\

由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击。比如设定每分钟最多可以请求100次某个接口,如12:00:00-12:00:59时间段内没有数据请求,而12:00:59-12:01:00时间段内突然并发100次请求,而紧接着跨入下一个计数周期,计数器清零,在12:01:00-12:01:01内又有100次请求。那么也就是说在时间临界点左右可能同时有2倍的阀值进行请求,从而造成后台处理请求过载的情况,导致系统运营能力不足,甚至导致系统崩溃。

  1. 滑动时间窗口算法

\

滑动窗口算法是把固定时间片进行划分,并且随着时间移动,移动方式为开始时间点变为时间列表中的第二时间点,结束时间点增加一个时间点,不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。

滑动窗口算法可以有效的规避计数器算法中时间临界点的问题,但是仍然存在时间片段的概念。同时滑动窗口算法计数运算也相对固定时间窗口算法比较耗时。滑动时间窗口算法,如图4-7所示。

图4-7 滑动时间窗口算法

4.2 Resilience4j简介

4.2.1 Resilience4j简介

Netflix的Hystrix微服务容错库已经停止更新,官方推荐使用Resilience4j代替Hystrix,或者使用Spring Cloud Alibaba的Sentinel组件。

Resilience4j是受到Netflix Hystrix的启发,为Java8和函数式编程所设计的轻量级容错框架。整个框架只是使用了Varr的库,不需要引入其他的外部依赖。与此相比,Netflix Hystrix对Archaius具有编译依赖,而Archaius需要更多的外部依赖,例如Guava和Apache Commons Configuration。

Resilience4j提供了提供了一组高阶函数(装饰器),包括断路器,限流器,重试机制,隔离机制。你可以使用其中的一个或多个装饰器对函数式接口,lambda表达式或方法引用进行装饰。这么做的优点是你可以选择所需要的装饰器进行装饰。

在使用Resilience4j的过程中,不需要引入所有的依赖,只引入需要的依赖即可。

核心模块

  • resilience4j-circuitbreaker: 熔断
  • resilience4j-ratelimiter: 限流
  • resilience4j-bulkhead: 隔离
  • resilience4j-retry: 自动重试
  • resilience4j-cache: 结果缓存
  • resilience4j-timelimiter: 超时处理

4.2.2 Resilience4j和Hystrix的异同

\

  • Hystrix使用HystrixCommand来调用外部的系统,而R4j提供了一些高阶函数,例如断路器、限流器、隔离机制等,这些函数作为装饰器对函数式接口、lambda表达式、函数引用进行装饰。此外,R4j库还提供了失败重试和缓存调用结果的装饰器。你可以在函数式接口、lambda表达式、函数引用上叠加地使用一个或多个装饰器,这意味着隔离机制、限流器、重试机制等能够进行组合使用。这么做的优点在于,你可以根据需要选择特定的装饰器。任何被装饰的方法都可以同步或异步执行,异步执行可以采用 CompletableFuture 或RxJava。
  • 当有很多超过规定响应时间的请求时,在远程系统没有响应和引发异常之前,断路器将会开启。
  • 当Hystrix处于半开状态时,Hystrix根据只执行一次请求的结果来决定是否关闭断路器。而R4j允许执行可配置次数的请求,将请求的结果和配置的阈值进行比较来决定是否关闭断路器。
  • R4j提供了自定义的Reactor和Rx Java操作符对断路器、隔离机制、限流器中任何的反应式类型进行装饰。
  • Hystrix和R4j都发出一个事件流,系统可以对发出的事件进行监听,得到相关的执行结果和延迟的时间统计数据都是十分有用的。

\

4.3 Resilence4j最佳实践

4.3.1 添加依赖

\

在订单工程的pom.xml中,添加依赖如下。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>

        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-bulkhead</artifactId>
            <version>1.7.0</version>
        </dependency>

\

注意Resilience4j的bulkhead模块需要手动添加,否则项目不具备bulkhead隔离功能,如果需要添加其他Resilience4j模块,参考添加即可。

4.2.2 断路器(CircuitBreaker)

\

  1. CircuitBreaker简介

\

断路器通过有限状态机实现,有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF_OPEN),还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED_OPEN)。如图4-8所示。

图4-8 熔断机模型图

\

当熔断器关闭时,所有的请求都会通过熔断器。如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率,如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。

断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。

除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。这两个状态不会生成熔断器事件(除状态装换外),并且不会记录事件的成功或者失败。退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

  1. CircuitBreaker配置

\

断路器配置属性,如表4-2所示。

配置属性默认值描述
failureRateThreshold50以百分比配置失败率阈值。当失败率等于或大于阈值时,断路器状态并关闭变为开启,并进行服务降级。
slowCallRateThreshold100以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于阈值时,断路器开启,并进行服务降级。
slowCallDurationThreshold60000 [ms]配置调用时间的阈值,高于该阈值的呼叫视为慢调用,并增加慢调用比例。
permittedNumberOfCallsInHalfOpenState10断路器在半开状态下允许通过的调用次数。
maxWaitDurationInHalfOpenState0断路器在半开状态下的最长等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会一直处于半开状态,直到所有允许通过的访问结束。
slidingWindowTypeCOUNT_BASED配置滑动窗口的类型,当断路器关闭时,将调用的结果记录在滑动窗口中。滑动窗口的类型可以是count-based或time-based。如果滑动窗口类型是COUNT_BASED,将会统计记录最近slidingWindowSize次调用的结果。如果是TIME_BASED,将会统计记录最近slidingWindowSize秒的调用结果。
slidingWindowSize100配置滑动窗口的大小。
minimumNumberOfCalls100断路器计算失败率或慢调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls为10,则必须至少记录10个调用,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
waitDurationInOpenState60000 [ms]断路器从开启过渡到半开应等待的时间。
automaticTransition FromOpenToHalfOpenEnabledfalse如果设置为true,则意味着断路器将自动从开启状态过渡到半开状态,并且不需要调用来触发转换。创建一个线程来监视断路器的所有实例,以便在WaitDurationInOpenstate之后将它们转换为半开状态。但是,如果设置为false,则只有在发出调用时才会转换到半开,即使在waitDurationInOpenState之后也是如此。这里的优点是没有线程监视所有断路器的状态。
recordExceptionsempty记录为失败并因此增加失败率的异常列表。 除非通过ignoreExceptions显式忽略,否则与列表中某个匹配或继承的异常都将被视为失败。 如果指定异常列表,则所有其他异常均视为成功,除非它们被ignoreExceptions显式忽略。
ignoreExceptionsempty被忽略且既不算失败也不算成功的异常列表。 任何与列表之一匹配或继承的异常都不会被视为失败或成功,即使异常是recordExceptions的一部分。
recordExceptionthrowable -> true· By default all exceptions are recored as failures.一个自定义断言,用于评估异常是否应记录为失败。 如果异常应计为失败,则断言必须返回true。如果出断言返回false,应算作成功,除非ignoreExceptions显式忽略异常。
ignoreExceptionthrowable -> false By default no exception is ignored.自定义断言来判断一个异常是否应该被忽略,如果应忽略异常,则谓词必须返回true。 如果异常应算作失败,则断言必须返回false。

表4-2 熔断属性列表\

参考表4-2属性,在订单项目中配置断路器,代码如下。

  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 30 #失败请求百分比,超过这个比例,CircuitBreaker变为OPEN状态
        slidingWindowSize: 10 #滑动窗口的大小,配置COUNT_BASED,表示10个请求,配置TIME_BASED表示10秒
        minimumNumberOfCalls: 5 #最小请求个数,只有在滑动窗口内,请求个数达到这个个数,才会触发CircuitBreader对于断路器的判断
        slidingWindowType: TIME_BASED #滑动窗口的类型
        permittedNumberOfCallsInHalfOpenState: 3 #当CircuitBreaker处于HALF_OPEN状态的时候,允许通过的请求个数
        automaticTransitionFromOpenToHalfOpenEnabled: true #设置true,表示自动从OPEN变成HALF_OPEN,即使没有请求过来
        waitDurationInOpenState: 2s #从OPEN到HALF_OPEN状态需要等待的时间
        recordExceptions: #异常名单
          - java.lang.Exception
    instances:
      backendA:
        baseConfig: default #熔断器backendA,继承默认配置default
      backendB:
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的呼叫视为慢调用,并增加慢调用比例。
        slowCallRateThreshold: 30 #慢调用百分比阈值,断路器把调用时间大于slowCallDurationThreshold,视为慢调用,当慢调用比例大于阈值,断路器打开,并进行服务降级
        slidingWindowSize: 10
        slidingWindowType: TIME_BASED
        minimumNumberOfCalls: 2
        permittedNumberOfCallsInHalfOpenState: 2
        waitDurationInOpenState: 120s #从OPEN到HALF_OPEN状态需要等待的时间

上面配置了2个断路器"backendA",和"backendB",其中backendA断路器配置基于default配置,"backendB"断路器配置了慢调用比例熔断,"backendA"熔断器配置了异常比例熔断

\

  1. OrderController

修改OrderController代码,以测试异常比例熔断和慢调用比例熔断效果,代码如下。

    @GetMapping("/payment/{id}")
    @CircuitBreaker(name = "backendD", fallbackMethod = "fallback")
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        log.info("now i enter the method!!!");

        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断

        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);

        log.info("now i exist the method!!!");

        return ResponseEntity.ok(payment);
    }

    public ResponseEntity<Payment> fallback(Integer id, Throwable e) {
        e.printStackTrace();
        Payment payment = new Payment();
        payment.setId(id);
        payment.setMessage("fallback...");
        return new ResponseEntity<>(payment, HttpStatus.BAD_REQUEST);
    }

注意name="backendA"和name="backendD"效果相同,当找不到配置的backendD熔断器,使用默认熔断器配置,即为"default"。

  1. 启动并测试

分别启动Eureka,支付服务,订单服务。

使用JMeter并发测试,创建线程组,如图4-9所示。

图4-9 JMeter线程组配置

创建HTTP请求、查看结果数,HTTP请求配置,如图4-10所示

图4-10 JMeter请求参数配置

正常执行效果如图 4-11所示。

图4-11 正常执行效果

此时关闭支付微服务,这时订单服务无法调用,所有请求报错,这时第一次并发发送20次请求,触发异常比例熔断,断路器进入打开状态,2s后(waitDurationInOpenState: 2s),断路器自动进入半开状态(automaticTransitionFromOpenToHalfOpenEnabled: true),再次发送请求断路器处于半开状态,允许3次请求通过(permittedNumberOfCallsInHalfOpenState: 3),注意此时控制台打印3次日志信息,说明半开状态,进入了3次请求调用,接着断路器继续进入打开状态。如图4-12所示。

图4-12 backendA异常比例熔断

注意name="backendA"和name="backendD",效果相同。

接下来测试慢比例调用熔断,修改OrderController代码,使用"backendB"熔断器,因为backendB熔断器,配置了慢比例调用熔断,然后启动Eureka,订单微服务和支付微服务。第一次发送并发发送了20个请求,触发了慢比例熔断,但是因为没有配置(automaticTransitionFromOpenToHalfOpenEnabled: true),无法自动从打开状态转为半开状态,需要浏览器中执行一次请求,这时,断路器才能从打开状态进入半开状态,接下来进入半开状态,根据配置,允许2次请求在半开状态通过(permittedNumberOfCallsInHalfOpenState: 2),第二次调用效果如图4-13所示。

\

图4-13 backendB慢比例调用熔断

4.2.3 隔离(Builkhead)

\

Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。

  • SemaphoreBulkhead使用了信号量
  • FixedThreadPoolBulkhead使用了有界队列和固定大小线程池
  1. 信号量隔离

SemaphoreBulkhead使用了信号量,配置属性,如下表4-3所示。

配置属性默认值描述
maxConcurrentCalls25隔离允许线程并发执行的最大数量
maxWaitDuration0当达到并发调用数量时,新的线程执行时将被阻塞,这个属性表示最长的等待时间。

表4-3 信号量隔离配置列表

在订单工程application.yml配置如下。

resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 5 # 隔离允许并发线程执行的最大数量
        maxWaitDuration: 20ms # 当达到并发调用数量时,新的线程的阻塞时间
    instances:
      backendA:
        baseConfig: default
      backendB:
        maxWaitDuration: 10ms
        maxConcurrentCalls: 20

修改OrderController代码如下。

\

    @GetMapping("/payment/{id}")
    @Bulkhead(name = "backendA", fallbackMethod = "fallback", type = Bulkhead.Type.SEMAPHORE)
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        log.info("now i enter the method!!!");

        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断

        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);

        log.info("now i exist the method!!!");

        return ResponseEntity.ok(payment);
    }

\

注意type默认为Bulkhead.Type.SEMAPHORE,表示信号量隔离

执行并测试,可以看到因为并发线程数为5(maxConcurrentCalls: 5),只有5个线程进入执行,其他请求降直接降级。效果如图4-14所示。

图4-14 信号量隔离效果

\

  1. 线程池隔离

FixedThreadPoolBulkhead配置如下,如下表4-4所示。

配置名称默认值含义
maxThreadPoolSizeRuntime.getRuntime().availableProcessors()配置最大线程池大小
coreThreadPoolSizeRuntime.getRuntime().availableProcessors() - 1配置核心线程池大小
queueCapacity100配置队列的容量
keepAliveDuration20ms当线程数大于核心时,这是多余空闲线程在终止前等待新任务的最长时间

表4-4 线程池隔离配置列表

支付工程的application.yml,配置如下。

resilience4j:
  thread-pool-bulkhead:
    configs:
      default:
        maxThreadPoolSize: 4 # 最大线程池大小
        coreThreadPoolSize: 2 # 核心线程池大小
        queueCapacity: 2 # 队列容量
    instances:
      backendA:
        baseConfig: default
      backendB:
        maxThreadPoolSize: 1
        coreThreadPoolSize: 1
        queueCapacity: 1

增加OrderService,注意,FixedThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。代码如下。

@Service
@Slf4j
public class OrderService {

    @Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<Payment> getPaymet() throws InterruptedException {
        log.info("now i enter the method!!!");
        Thread.sleep(10000L);
        log.info("now i exist the method!!!");
        return CompletableFuture.supplyAsync(() -> new Payment(123, "线程池隔离回退。。。"));
    }

}

修改OrderController代码如下。

\

    @Autowired
    private OrderService orderService;

	@GetMapping("/payment/{id}")
    @Bulkhead(name = "backendA", fallbackMethod = "fallback", type = Bulkhead.Type.THREADPOOL)
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        return ResponseEntity.ok(orderService.getPaymet().get());
    }

\

执行并测试,可以看到因为最大线程数为4(maxThreadPoolSize: 4 ),只有4个线程进入执行,其他请求降直接降级。效果如图4-15所示。

图4-15 线程池隔离效果

\

4.2.4 限流(RateLimiter)

R4的限流模块RateLimter基于滑动窗口,和令牌桶限流算法,配置如下,如下表4-5所示。

属性默认值描述
timeoutDuration5秒线程等待权限的默认等待时间
limitRefreshPeriod500纳秒限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod。
limitForPeriod50在一次刷新周期内,允许执行的最大请求数

表4-5 限流配置列表

在订单工程的application.yml中配置如下。

resilience4j:
  ratelimiter:
    configs:
      default:
        timeoutDuration: 5 # 线程等待权限的默认等待时间
        limitRefreshPeriod: 1s # 限流器每隔1s刷新一次,将允许处理的最大请求重置为2
        limitForPeriod: 2 #在一个刷新周期内,允许执行的最大请求数
    instances:
      backendA:
        baseConfig: default
      backendB:
        timeoutDuration: 5
        limitRefreshPeriod: 1s
        limitForPeriod: 5

\

修改OrderController代码如下。

\

    @GetMapping("/payment/{id}")
    @RateLimiter(name = "backendA", fallbackMethod = "fallback")
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        log.info("now i enter the method!!!");

        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断

        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);

        log.info("now i exist the method!!!");

        return ResponseEntity.ok(payment);
    }

启动并测试,因为在一个刷新周期1s(limitRefreshPeriod: 1s)允许执行的最大请求数为2(limitForPeriod: 2),因此,并发发送20个请求后,只有2个请求通过限流器的限制,效果如图4-16所示。

图4-6 限流效果