在調用三方接口時,我們一般要考慮接口調用失敗的處理,可以通過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級別, 找到關鍵輸出的日志對應的類 先打上斷點 再把上下源碼一行一行跟蹤