在调用三方接口时,我们一般要考虑接口调用失败的处理,可以通过spring提供的retry来实现;如果重试几次都失败了,可能就要考虑降级补偿了;
有时我们也可能要考虑熔断,在微服务中可能会使用sentinel来做熔断;在单体服务中,可以使用轻量化的resilience4j来做限流或熔断
文章目录
- maven依赖
- retry重试机制
- circuitbreaker 熔断机制
- ratelimiter限流机制
maven依赖
<!-- web项目jar包 包含starter、spring、webmvc 等--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- aop --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- spring提供的重试机制 需要@EnableRetry 注解开启 --><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><!-- actuator 有健康检查、监控管理等生产环境需要使用到的功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- resilience4j 如果是Spring Boot 2.x项目,使用resilience4j-spring-boot2替代 --><dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-spring-boot3</artifactId><version>2.2.0</version></dependency>
retry重试机制
- 引入了retry相关依赖后,需要开启@EnableRetry
例如:
@SpringBootApplication
@MapperScan
@EnableRetry
public class Application {public static void main(String[] args) {SpringApplication.run(Application .class, args);}}
- 在要重试的方法加上@Retryable注解
例如:
@RestController
@RequestMapping("/test/retry")
public class RetryController {@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test")public String test() {System.out.println("do-something:"+ LocalDateTime.now());long l = System.currentTimeMillis();System.out.println(count++);int a = 0;System.out.println( l % 2 == 0);if (l % 2 == 0) {a = 1 / 0;}System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}}
参数解释:
value:抛出指定异常才会重试
noRetryFor:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略,默认使用@Backoff,
@Backoff的value(相当于delay)表示隔多少毫秒后重试,默认为1000L;
multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试。
tips: multiplier 的实际意义 当我们调用某个接口失败时,如果紧接着马上又调用,大概率接口仍然是失败的,multiplier是一个递延时间,可以起到调用间隔越来越大的作用。
- 如果重试到最大次数仍然失败,希望有降级处理 代码则变成:
@RestController
@RequestMapping("/test/retry")
public class RetryController {@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test")public String test() {System.out.println("do-something:"+ LocalDateTime.now());long l = System.currentTimeMillis();System.out.println(count++);int a = 0;System.out.println( l % 2 == 0);if (l % 2 == 0) {a = 1 / 0;}System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}/*** @Recover 的返回类型,必须跟 @Retryable修饰的方法返回值一致。*/@Recoverpublic String recoverTest(ArithmeticException e) {System.out.println("test模拟记录错误日志"+e.getMessage());return "test降级处理";}}
- 如果一个类中,有多个方法呢?@Retryable和@Recover是怎么对应的?(即@Recover是怎么判断 来自哪个方法):
@Recover方法必须与@Retryable方法在同一个Spring管理的Bean中;确保AOP代理生效。
当存在多个可能的@Recover方法时,Spring按以下优先级选择:
异常类型最具体的方法(如子类异常优先于父类)。
返回类型最匹配的方法(避免类型转换错误)。
参数列表更匹配的方法(如包含原方法参数)
例如:
@RestController
@RequestMapping("/test/retry")
public class RetryController {int count = 0;String noete = """@Recover方法必须与@Retryable方法在同一个Spring管理的Bean中,确保AOP代理生效。当存在多个可能的@Recover方法时,Spring按以下优先级选择:异常类型最具体的方法(如子类异常优先于父类)。返回类型最匹配的方法(避免类型转换错误)。参数列表更匹配的方法(如包含原方法参数)value:抛出指定异常才会重试noRetryFor:指定不处理的异常maxAttempts:最大重试次数,默认3次backoff:重试等待策略,默认使用@Backoff,@Backoff的value(相当于delay)表示隔多少毫秒后重试,默认为1000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试。""";@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@CircuitBreaker(name = "backendA",fallbackMethod = "fallback")@GetMapping("/test")public String test() {System.out.println("do-something:"+ LocalDateTime.now());long l = System.currentTimeMillis();System.out.println(count++);int a = 0;System.out.println( l % 2 == 0);if (l % 2 == 0) {a = 1 / 0;}System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test1")public String test1() {System.out.println("do-something:"+ LocalDateTime.now());// NumberFormatExceptionInteger a = Integer.parseInt(null);System.out.println("res=" + a +" "+LocalDateTime.now());return "200";}@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test2")public String test2(String name) {System.out.println("do-something:"+ LocalDateTime.now());int a = 1 / 0;System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}/*** @Recover 的返回类型,必须跟 @Retryable修饰的方法返回值一致。*/@Recoverpublic String recoverTest(ArithmeticException e) {System.out.println("test模拟记录错误日志"+e.getMessage());return "test降级处理";}@Recoverpublic String recoverTest1(NumberFormatException e) {System.out.println("test1模拟记录错误日志"+e.getMessage());return "test1降级处理";}@Recoverpublic String recoverTest2(ArithmeticException e,String name) {System.out.println("test2模拟记录错误日志"+e.getMessage());return "test2降级处理";}}
circuitbreaker 熔断机制
- yml中配置
# resilience4j (轻量级熔断)
resilience4j:circuitbreaker:instances:# 自定义的熔断名称backendA:sliding-window-type: count_based # 默认是count, 还可以配置 TIME_BASED sliding-window-size则表示最近几秒sliding-window-size: 4 # 只看最近四次调用(为了方便测试)minimum-number-of-calls: 1 #只需一次调用就开始评估failure-rate-threshold: 50 # 失败率超过50 就开启熔断 (开启熔断后,后面请求就直接进入熔断了)wait-duration-in-open-state: 5s # 开启后5s进入半开状态 (半开启是个灵活的状态,后续服务恢复就不用进入熔断了)permitted-number-of-calls-in-half-open-state: 2 # 半开状态允许有两次测试调用 如果低于 failure-rate-threshold 失败率 ,则不会进入熔断automatic-transition-from-open-to-half-open-enabled: true # 半开启状态
- 代码中使用,fallback如果是只接收限流异常 则定义成CallNotPermittedException,如果定义成Exception , 则只要发生异常就会进入方法(不会基于yml的配置),如何定义取决于业务需要。
@RestController
@RequestMapping("/test/retry")
public class RetryController {String noete = """@Recover方法必须与@Retryable方法在同一个Spring管理的Bean中,确保AOP代理生效。当存在多个可能的@Recover方法时,Spring按以下优先级选择:异常类型最具体的方法(如子类异常优先于父类)。返回类型最匹配的方法(避免类型转换错误)。参数列表更匹配的方法(如包含原方法参数)value:抛出指定异常才会重试noRetryFor:指定不处理的异常maxAttempts:最大重试次数,默认3次backoff:重试等待策略,默认使用@Backoff,@Backoff的value(相当于delay)表示隔多少毫秒后重试,默认为1000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试。""";@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))// name指定的值和我们yml配置保持一致@CircuitBreaker(name = "backendA",fallbackMethod = "fallback")@GetMapping("/test")public String test() {System.out.println("do-something:"+ LocalDateTime.now());long l = System.currentTimeMillis();int a = 0;System.out.println( l % 2 == 0);if (l % 2 == 0) {a = 1 / 0;}System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}/*** 熔断器指定的方法* 返回值类型也要一致*/public String fallback(CallNotPermittedException e) {System.out.println("进入熔断");return "进入了熔断";}
}
相关源码:
- @Retryable+@CircuitBreaker 一起使用注意事项:
Recover 执行顺序 > fallbackMethod ;
如果 @Recover 吞了异常(即没有手动抛出异常) 是不会再进入fallbackMethod 的,所以很可能造成@Retryable+@CircuitBreaker 一起使用 导致CircuitBreaker失效。如果一定要一起使用,我们可以在Recover把异常抛出去
完整版测试代码:
@RestController
@RequestMapping("/test/retry")
public class RetryController {String noete = """@Recover方法必须与@Retryable方法在同一个Spring管理的Bean中,确保AOP代理生效。当存在多个可能的@Recover方法时,Spring按以下优先级选择:异常类型最具体的方法(如子类异常优先于父类)。返回类型最匹配的方法(避免类型转换错误)。参数列表更匹配的方法(如包含原方法参数)value:抛出指定异常才会重试noRetryFor:指定不处理的异常maxAttempts:最大重试次数,默认3次backoff:重试等待策略,默认使用@Backoff,@Backoff的value(相当于delay)表示隔多少毫秒后重试,默认为1000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试。""";@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@CircuitBreaker(name = "backendA",fallbackMethod = "fallback")@GetMapping("/test")public String test() {System.out.println("do-something:"+ LocalDateTime.now());long l = System.currentTimeMillis();int a = 0;System.out.println( l % 2 == 0);if (l % 2 == 0) {a = 1 / 0;}System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test1")public String test1() {System.out.println("do-something:"+ LocalDateTime.now());// NumberFormatExceptionInteger a = Integer.parseInt(null);System.out.println("res=" + a +" "+LocalDateTime.now());return "200";}@Retryable(value = Exception.class, maxAttempts = 3,backoff = @Backoff(delay = 1000,multiplier = 1.5))@GetMapping("/test2")public String test2(String name) {System.out.println("do-something:"+ LocalDateTime.now());int a = 1 / 0;System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}/*** @Recover 的返回类型,必须跟 @Retryable修饰的方法返回值一致。* 注意 Recover 执行顺序 > fallbackMethod ; 如果 @Recover 吞了异常(即没有手动抛出) 是不会再进入fallbackMethod 的* 如果一定要 Recover + fallbackMethod 同时使用,可以在Recover 把异常抛出去*/@Recoverpublic String recoverTest(ArithmeticException e) {System.out.println("test模拟记录错误日志"+e.getMessage());throw e;
// return "test降级处理";}@Recoverpublic String recoverTest1(NumberFormatException e) {System.out.println("test1模拟记录错误日志"+e.getMessage());return "test1降级处理";}@Recoverpublic String recoverTest2(ArithmeticException e,String name) {System.out.println("test2模拟记录错误日志"+e.getMessage());return "test2降级处理";}/*** 熔断器指定的方法* 返回值类型也要一致*/public String fallback(CallNotPermittedException e) {System.out.println("进入熔断");return "进入了熔断";}
ratelimiter限流机制
- yml配置
限流和熔断的配置是类似的:
# resilience4j (轻量级熔断)
resilience4j:# 限流配置ratelimiter:instances:# 自定义的限流名称commonRateLimiter:limitForPeriod: 10 # 每个刷新周期内允许的最大请求数limitRefreshPeriod: 1s # 限流刷新周期timeoutDuration: 100ms # 获取许可的等待超时时间registerHealthIndicator: true # 是否注册健康指标eventConsumerBufferSize: 100 # 事件缓冲区大小# 熔断配置 circuitbreaker:instances:# 自定义的熔断名称backendA:sliding-window-type: count_based # 默认是count, 还可以配置 TIME_BASED sliding-window-size则表示最近几秒sliding-window-size: 4 # 只看最近四次调用(为了方便测试)minimum-number-of-calls: 1 #只需一次调用就开始评估failure-rate-threshold: 50 # 失败率超过50 就开启熔断 (开启熔断后,后面请求就直接进入熔断了)wait-duration-in-open-state: 5s # 开启后5s进入半开状态 (半开启是个灵活的状态,后续服务恢复就不用进入熔断了)permitted-number-of-calls-in-half-open-state: 2 # 半开状态允许有两次测试调用 如果低于 failure-rate-threshold 失败率 ,则不会进入熔断automatic-transition-from-open-to-half-open-enabled: true # 半开启状态
- 代码中使用,注意限流的补偿方法入参需要定义成RequestNotPermitted
@GetMapping("/limit")@RateLimiter(name = "commonRateLimiter",fallbackMethod = "limit")public String limitTest() {System.out.println("do-something:"+ LocalDateTime.now());int a = 1 / 0;System.out.println("res=" + a+" "+LocalDateTime.now());return "200";}/*** 注意入参根据业务情况 定义RequestNotPermitted还是Exception * 如果定义成Exception则被@RateLimiter修饰的方法 一旦发生异常就会进入该方法,而不是优先读取yml的配置* @param e RequestNotPermitted*/public String limit(RequestNotPermitted e) {System.out.println("被限流了");return "被限流了";}
}
- 简单看一下源码,看看为什么要定义成相关异常
io.github.resilience4j.ratelimiter.RateLimiter 类:
下面这行代码表示被限流了会抛出RequestNotPermitted 异常;
下面这行则是fallback的一个公用处理,会去找到接收这个异常的方法
tips: 为什么能找到源码位置? 首先把logging调成debug级别, 找到关键输出的日志对应的类 先打上断点 再把上下源码一行一行跟踪