Skip to content

弹性特性 (Resilience Features)

自 Spring Framework 7.0 起,核心框架引入了通用的弹性(Resilience)特性。特别是针对方法调用的 @Retryable@ConcurrencyLimit 注解,以及编程式重试支持

@Retryable

@Retryable 是一个注解,用于指定单个方法(声明在方法级别)或给定类层次结构中所有经代理调用的方法(声明在类型级别)的重试特性。

java
@Retryable
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

默认情况下,方法调用在抛出任何异常时都会进行重试:初始失败后最多重试 3 次(maxRetries = 3),且重试间隔为 1 秒。

TIP

@Retryable 方法将至少执行一次,且最多重试 maxRetries 次,其中 maxRetries 是最大重试尝试次数。 具体而言:总尝试次数 = 1 次初始尝试 + maxRetries 次重试尝试

例如,如果 maxRetries 设置为 4,则该方法至少执行 1 次,最多执行 5 次。

如有必要,可以为每个方法进行特定的适配——例如,通过 includesexcludes 属性缩小需要重试的异常范围。提供的异常类型将与失败调用抛出的异常以及嵌套的异常原因进行匹配。

java
@Retryable(MessageDeliveryException.class)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

TIP

@Retryable(MessageDeliveryException.class)@Retryable(includes = MessageDeliveryException.class) 的简写形式。

提示

对于高级用例,你可以通过 @Retryable 中的 predicate 属性指定自定义的 MethodRetryPredicate。该断言将根据 Method 和给定的 Throwable 来决定是否重试失败的方法调用(例如,通过检查 Throwable 的消息)。

自定义断言可以与 includesexcludes 结合使用;但是,自定义断言总是在应用了 includesexcludes 过滤之后才会被应用。

或者配置 4 次重试尝试,并带有一定抖动(Jitter)的指数退避(Exponential Back-off)策略:

java
@Retryable(
  includes = MessageDeliveryException.class,
  maxRetries = 4,
  delay = 100,
  jitter = 10,
  multiplier = 2,
  maxDelay = 1000)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

最后同样重要的是,@Retryable 也适用于具有响应式返回类型的响应式方法,它会使用 Reactor 的重试功能来增强管道:

java
@Retryable(maxRetries = 4, delay = 100)
public Mono<Void> sendNotification() {
    return Mono.from(...); // (1)
}
  1. 原始的 Mono 将会被注入重试规范(Retry Spec)。

有关各种特性的详细信息,请参阅 @Retryable 的注解属性。

提示

@Retryable 中的几个属性具有 String 变体,提供属性占位符和 SpEL 支持,作为上述示例中具体类型属性的替代方案。

@ConcurrencyLimit

@ConcurrencyLimit 是一个注解,用于指定单个方法(声明在方法级别)或给定类层次结构中所有代理调用的方法(声明在类型级别)的并发限制。

java
@ConcurrencyLimit(10)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

这旨在保护目标资源免受过多线程同时访问,类似于线程池或连接池的池大小限制效果,如果达到限制则阻塞访问。

你可以可选地将限制设置为 1,从而有效地锁定对目标 Bean 实例的访问:

java
@ConcurrencyLimit(1)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

这种限制在虚拟线程 (Virtual Threads) 场景下特别有用,因为虚拟线程通常没有到位的线程池限制。对于异步任务,可以通过 SimpleAsyncTaskExecutor 进行约束。对于同步调用,此注解通过 ConcurrencyThrottleInterceptor 提供等效行为(该拦截器自 Spring Framework 1.0 起就已提供给 AOP 框架编程式使用)。

提示

@ConcurrencyLimit 也有一个 limitString 属性,支持属性占位符和 SpEL。

启用弹性方法 (Enabling Resilient Methods)

与 Spring 许多核心基于注解的特性一样,@Retryable@ConcurrencyLimit 被设计为元数据,你可以选择尊重或忽略。启用弹性注解处理最便捷的方法是在对应的 @Configuration 类上声明 @EnableResilientMethods

或者,可以通过在上下文中显式定义 RetryAnnotationBeanPostProcessorConcurrencyLimitBeanPostProcessor Bean 来分别启用这些注解。

编程式重试支持 (Programmatic Retry Support)

与提供声明式方法的 @Retryable 不同,RetryTemplate 为重试任意代码块提供了编程式 API。

具体而言,RetryTemplate 根据配置的 RetryPolicy 执行并可能重试一个操作。

java
var retryTemplate = new RetryTemplate(); // (1)

retryTemplate.execute(
        () -> jmsClient.destination("notifications").send(...));
  1. 隐式使用 RetryPolicy.withDefaults()

默认情况下,重试操作对抛出的任何异常都会生效:初始失败后最多重试 3 次,间隔 1 秒。

如果你只需要自定义重试次数,可以使用 RetryPolicy.withMaxRetries() 工厂方法:

TIP

重试操作将至少执行一次,且最多重试 maxRetries 次。 总尝试次数 = 1 次初始尝试 + maxRetries 次重试尝试

java
var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // (1)

retryTemplate.execute(
        () -> jmsClient.destination("notifications").send(...));
  1. 显式使用 RetryPolicy.withMaxRetries(4)

如果需要缩小重试异常范围,可以通过 includes()excludes() 构建器方法实现。

java
var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class) // (1)
        .excludes(...) // (2)
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.execute(
        () -> jmsClient.destination("notifications").send(...));
  1. 指定一个或多个包含的异常类型。
  2. 指定一个或多个排除的异常类型。

以下显示了如何配置一个带有 4 次重试、带抖动的指数退避策略的 RetryPolicy

java
var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class)
        .maxRetries(4)
        .delay(Duration.ofMillis(100))
        .jitter(Duration.ofMillis(10))
        .multiplier(2)
        .maxDelay(Duration.ofSeconds(1))
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.execute(
        () -> jmsClient.destination("notifications").send(...));

提示

可以向 RetryTemplate 注册 RetryListener 以对关键重试阶段发生的事件作出反应,你可以通过 CompositeRetryListener 组合多个监听器。

虽然工厂方法和构建器 API 覆盖了大部分场景,你也可以实现自定义 RetryPolicy 以完全控制重试触发条件和 BackOff 策略。


补充教学

1. Spring 7.0 为什么要内置弹性方案?

在 Spring 7.0 之前,Java 开发者通常有两种选择来实现重试和限流:

  • Spring Retry:一个独立的 Spring 库。
  • Resilience4j:一个功能更强大、更通用的弹性库。

Spring 7.0 将这些核心能力收编进 spring-corespring-context,意味着:

  • 零额外依赖:你不再需要引入外部库就能获得生产级的重试和并发控制。
  • 更好的原生支持:例如与 虚拟线程 的无缝集成,以及对 响应式流 (Project Reactor) 的原生内置支持(无需额外适配器)。

2. 重试机制的关键:幂等性 (Idempotency)

在使用 @Retryable 前,必须自问:我的操作是幂等的吗?

  • 非幂等操作:如果 sendNotification 是向数据库扣钱,第一次超时但实际扣成功了,重试会导致扣两次。
  • 最佳实践
    • 只对读操作或已知幂等的写操作重试(如:根据 UUID 更新状态)。
    • 结合分布式锁幂等令牌

3. @ConcurrencyLimit 与虚拟线程的“天作之合”

在 Platform Thread(平台线程)时代,我们用固定大小的线程池来限流。但在虚拟线程时代:

  • 线程是廉价且海量的(上百万个)。
  • 如果你不对外部受限资源(如连接数有限的旧数据库)加锁,海量的虚拟线程可能会瞬间冲垮下游。
  • @ConcurrencyLimit 的价值在于:它不限制你开了多少虚拟线程,而是通过底层的信号量(Semaphore)机制,限制同时进入该方法执行的线程数。

4. 退避策略 (Back-off) 为什么要加抖动 (Jitter)?

如果 100 个请求同时失败,且都设置了固定 1 秒的延迟,那么 1 秒后它们会再次同时尝试,这被称为“惊群效应(Thundering Herd)”。

  • Jitter:在延迟时间上增加一个随机范围(如 1s ± 100ms)。
  • 它能打散重试流量,让系统压力更平滑地释放。

5. 声明式 vs 编程式:如何选?

  • @Retryable:适用于业务逻辑层,配置简单,非侵入。
  • RetryTemplate:适用于底层集成层中间件开发,如果你需要在代码的不同路径里根据动态参数决定重试逻辑,或者需要手动控制 RetryContext

Based on Spring Framework.