bigywater.tech
Spring · 27

@Retryable:Spring Framework 7 内置的重试、退避与异常筛选(以及 @ConcurrencyLimit)

Spring 7 把重试做进了框架本身:@Retryable 默认怎么重试?退避怎么算?哪些异常会被重试?用完次数之后调用者看到什么?RetryTemplate 又是什么样?这篇在 Spring Framework 7.0.9 上用带时间戳的时间线逐项验证,也顺带验证了同包的 @ConcurrencyLimit。

1 加 3 次
默认 maxRetries = 3 时一个总是失败的方法最多执行的次数(delay 默认 1000 ms)
100、200、400、500、500
delay = 100、multiplier = 2、maxDelay = 500 时各次重试之间的等待(ms,实测约 108、204、404、505、505)
1219 对 305 ms
8 个线程调用耗时 300 ms 的方法,@ConcurrencyLimit(2) 的 BLOCK 与 REJECT 策略的总耗时

默认与基本用法

实测 · 本机执行输出
  --- 默认的 @Retryable(maxRetries = 3、delay = 1000 ms),前 2 次失败
      5 ms  defaults 第 1 次执行
   1014 ms  defaults 第 2 次执行
   2019 ms  defaults 第 3 次执行
   2020 ms  → 返回 ok@3
  --- @Retryable(maxRetries = 4, delay = 100),前 2 次失败
      0 ms  fast 第 1 次执行
    104 ms  fast 第 2 次执行
    206 ms  fast 第 3 次执行
    206 ms  → 返回 ok@3

@EnableResilientMethods 加 @Retryable,默认最多重试 3 次、间隔 1 秒。

退避

实测 · 本机执行输出
  --- maxRetries = 5, delay = 100, multiplier = 2, maxDelay = 500,总是失败
      5 ms  backoff 第 1 次执行
    113 ms  backoff 第 2 次执行
    317 ms  backoff 第 3 次执行
    721 ms  backoff 第 4 次执行
   1226 ms  backoff 第 5 次执行
   1731 ms  backoff 第 6 次执行
   1732 ms  → 抛 IllegalStateException: 失败 6

delay、multiplier、maxDelay 控制间隔,maxDelay 封顶。

异常筛选

实测 · 本机执行输出
  --- includes / excludes / 嵌套的原因 / predicate
      4 ms  onlyIo 执行(抛 IOException)
     62 ms  onlyIo 执行(抛 IOException)
    118 ms  onlyIo 执行(抛 IOException)
    173 ms  onlyIo 执行(抛 IOException)
    174 ms  → 抛 IOException: io
    174 ms  onlyIo 执行(抛 IllegalStateException)
    174 ms  → 抛 IllegalStateException: ise
    175 ms  excl 执行(抛 IllegalArgumentException)
    175 ms  → 抛 IllegalArgumentException: iae
    175 ms  excl 执行(抛 IllegalStateException)
    230 ms  excl 执行(抛 IllegalStateException)
    285 ms  excl 执行(抛 IllegalStateException)
    340 ms  excl 执行(抛 IllegalStateException)
    340 ms  → 抛 IllegalStateException: ise
    341 ms  nested 执行(抛 RuntimeException,cause 是 IOException)
    394 ms  nested 执行(抛 RuntimeException,cause 是 IOException)
    446 ms  nested 执行(抛 RuntimeException,cause 是 IOException)
    446 ms  → 抛 RuntimeException: 包了一层 ← 原因 IOException: 底层
    448 ms  pred 执行(消息 可重试:x)
    503 ms  pred 执行(消息 可重试:x)
    556 ms  pred 执行(消息 可重试:x)
    612 ms  pred 执行(消息 可重试:x)
    612 ms  → 抛 IllegalStateException: 可重试:x
    613 ms  pred 执行(消息 别重试)
    613 ms  → 抛 IllegalStateException: 别重试

includes、excludes、predicate 决定哪些异常重试,嵌套的 cause 也参与匹配。

用完与超时

实测 · 本机执行输出
  --- maxRetries = 2,总是失败:次数用完之后调用者看到什么
      4 ms  exhaust 执行
     62 ms  exhaust 执行
    118 ms  exhaust 执行
    119 ms  → 抛 IllegalStateException: 总是失败
  --- timeout = 500 ms、delay = 200 ms、maxRetries = 10,总是失败
      0 ms  withTimeout 执行
    204 ms  withTimeout 执行
    408 ms  withTimeout 执行
    408 ms  → 抛 IllegalStateException: 总是失败

次数用完抛原始异常,timeout 限制包括等待在内的总时间。

RetryTemplate

实测 · 本机执行输出
  --- RetryTemplate(maxRetries = 2、delay = 50 ms),前 2 次失败
      0 ms  RetryTemplate 执行第 1 次
     58 ms  RetryListener.beforeRetry
     58 ms  RetryTemplate 执行第 2 次
     58 ms  RetryListener.onRetryFailure(编程式失败 2)
    113 ms  RetryListener.beforeRetry
    113 ms  RetryTemplate 执行第 3 次
    114 ms  RetryListener.onRetrySuccess
    114 ms  → 返回 ok
  --- RetryTemplate 总是失败
      0 ms  RetryTemplate 执行(总是失败)
     55 ms  RetryListener.beforeRetry
     55 ms  RetryTemplate 执行(总是失败)
     55 ms  RetryListener.onRetryFailure(总是失败)
    110 ms  RetryListener.beforeRetry
    110 ms  RetryTemplate 执行(总是失败)
    110 ms  RetryListener.onRetryFailure(总是失败)
    110 ms  RetryListener.onRetryPolicyExhaustion
    111 ms  → 次数用完:RetryException: Retry policy for operation 'demo.Main$$Lambda' exhausted; aborting execution(最后的原因 IllegalStateException,suppressed 2 个)

编程式重试,监听器只管“重试”,次数用完抛 RetryException。

@ConcurrencyLimit

实测 · 本机执行输出
    @ConcurrencyLimit(2),8 个线程同时调用一个耗时 300 ms 的方法:成功 8 个,被拒绝 0 个,全部结束花了 1219 ms,方法内观察到的最大并发 2
    @ConcurrencyLimit(2, policy = REJECT),8 个线程同时调用一个耗时 300 ms 的方法:成功 2 个,被拒绝 6 个(InvocationRejectedException),全部结束花了 305 ms

限制同一个方法的并发数,多余的排队或拒绝。

分步演示

@Retryable 与 @ConcurrencyLimit
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用不带 Boot 的 Spring Framework 7.0.9 的 @EnableResilientMethods 与 spring-core 里的 org.springframework.core.retry(查了 7.0.9 的源码确认属性名和默认值);时间戳每次运行有几 ms 的抖动,间隔写的是实测值
    • 没有验证:独立项目 spring-retry 的 @Retryable / @Recover(Spring 7 内置的这一套没有 @Recover);jitter 抖动的效果;响应式返回类型(Mono、Flux)与 CompletableFuture 的重试;@Retryable 与 @Transactional 的组合顺序;maxRetriesString 等配置占位符;重试时的线程阻塞(重试期间是在调用线程里 sleep)
    京ICP备2026067502号-1京公网安备11010502063487号