
1. 项目概述为什么我们需要重试与降级在分布式系统和微服务架构成为主流的今天服务间的调用失败几乎成了家常便饭。网络抖动、数据库连接超时、第三方接口不稳定……任何一个环节的微小波动都可能导致一次请求的彻底失败。作为开发者我们无法保证外部依赖的绝对可靠但我们可以让我们的应用在面对失败时更加“坚韧”。想象一下你正在开发一个支付回调接口。用户支付成功后支付平台会调用你的接口来更新订单状态。如果这时你的数据库刚好因为一次短暂的网络分区而无法连接导致回调失败支付平台可能会认为你的服务不可用从而引发一系列复杂的对账和客诉问题。一个简单的重试机制可能就在这几秒钟内避免了后续数小时的麻烦。Spring框架的Retryable和Recover注解就是为了优雅地解决这类问题而生的。它们不是Spring Core的一部分而是来自Spring Retry这个子项目。这两个注解允许我们以声明式的方式为方法添加重试和失败降级逻辑将业务代码与容错逻辑彻底解耦。你不再需要写满屏的try-catch和for循环只需要几个简单的注解就能赋予方法“失败了再来一次”甚至“失败后优雅退场”的能力。这不仅仅是代码的简化更是对“弹性设计”这一架构理念的落地实践。2. 核心注解深度解析与设计思路2.1 Retryable定义重试策略的蓝图Retryable是整个重试机制的核心。它的作用就像一份“重试作战计划”贴在目标方法上告诉Spring“这个方法可能会失败如果失败了请按照我的计划重新发起进攻。”这个注解提供了多个参数来精细控制重试行为理解每个参数的含义是正确使用它的关键value/include:指定触发重试的异常类型。这是一个数组你可以列出所有你认为“可重试”的异常。例如Retryable(value {SQLException.class, IOException.class})意味着只有当方法抛出SQLException或IOException时才会重试。如果抛出的是NullPointerException则不会重试。value和include是别名作用相同。exclude:与include相反指定哪些异常不触发重试。这通常用于在捕获了父类异常后排除某些特定的子类异常。maxAttempts:最大重试次数。注意这个次数包含了第一次正常的调用。如果设置为3那么最多会执行1次原始调用 2次重试。backoff:重试的退避策略这是避免“惊群效应”的关键。它不是一个简单的数字而是一个Backoff注解其下又有子参数delay: 第一次重试前的延迟时间毫秒。multiplier: 延迟倍数。例如delay1000, multiplier2则第一次重试等待1秒第二次等待2秒第三次等待4秒以此类推。maxDelay: 最大延迟时间防止倍数增长后延迟时间过长。random: 是否在延迟时间上增加随机因子这能有效避免多个失败实例在同一时间点同时重试对下游服务造成二次冲击。listeners:指定重试监听器Bean的名称用于在重试生命周期重试开始、重试失败、重试成功等中插入自定义逻辑比如打日志、发告警。设计思路解析Retryable的设计充分体现了“约定优于配置”和“关注点分离”的思想。它将重试这一横切关注点Cross-Cutting Concern从业务逻辑中剥离出来。开发者只需关注“在什么条件下重试”和“如何重试”而“如何执行重试”这个复杂的流程异常捕获、等待、再次调用则由Spring Retry的AOP面向切面编程模块在背后默默完成。这种声明式的方式极大地提升了代码的可读性和可维护性。2.2 Recover重试失败后的安全网无论重试策略多么完善总会有耗尽所有尝试依然失败的情况。Recover注解就是为这种最坏情况准备的“安全网”或“降级方案”。它的核心规则非常严格必须牢记返回值类型必须兼容Recover标记的降级方法的返回值类型必须与对应的Retryable方法完全一致或者是其父类。这是Spring在运行时进行方法匹配的关键依据。参数列表必须兼容降级方法的第一个参数必须是Throwable类型或其子类用于接收导致最终失败的异常对象。其余参数需要与Retryable方法的参数列表在类型和顺序上完全匹配。Spring会原样传递过来。必须位于同一个类中Recover方法必须和它要降级的Retryable方法在同一个Spring管理的Bean里。这是当前实现的一个限制。一对一匹配当所有重试耗尽后Spring会根据抛出的异常类型和返回值类型在同一个类中寻找最匹配的Recover方法来执行。设计思路解析Recover的设计体现了“优雅降级”的容错理念。系统的目标不是永远不失败而是在失败时能够以可控的、对用户影响最小的方式进行处理。例如查询用户信息失败降级方法可以返回一个带有默认值的“兜底”用户对象调用积分服务失败可以记录日志并返回“积分更新已记录稍后处理”的提示而不是让整个业务流程中断。Recover将这种降级逻辑也进行了声明式管理使得业务代码的健壮性设计变得清晰而直观。2.3 注解协同工作流程理解了两者的定义后我们来看它们是如何协同工作的调用被Retryable标记的方法。方法执行如果成功直接返回流程结束。如果抛出异常Spring Retry的拦截器会捕获它。检查该异常是否匹配Retryable中定义的value/include且不在exclude中。如果匹配且当前重试次数未超过maxAttempts则根据backoff策略等待一段时间。等待结束后通过AOP代理再次调用原方法即重试。重复步骤3-6直到方法成功或重试次数用尽。如果重试次数用尽仍未成功Spring Retry会抛出最后一次的异常并开始寻找Recover降级方法。在同一Bean中寻找返回值类型兼容、第一个参数为最后一次异常类型或其父类、其余参数与原方法匹配的Recover方法。找到后执行该降级方法其返回值将作为整个调用的最终结果返回给调用者。如果未找到匹配的Recover方法则最终异常会被抛出。3. 实战配置与核心环节实现3.1 环境准备与依赖引入首先你需要在你的Spring Boot项目中引入spring-retry的依赖。对于Maven项目在pom.xml中添加dependency groupIdorg.springframework.retry/groupId artifactIdspring-retry/artifactId version2.0.5/version !-- 请使用最新稳定版本 -- /dependency !-- Spring Retry 需要AOP支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency对于Gradle项目在build.gradle中添加implementation org.springframework.retry:spring-retry:2.0.5 implementation org.springframework.boot:spring-boot-starter-aop然后在任意一个配置类上通常是主应用类SpringBootApplication标注的类或者一个专门的Configuration类添加EnableRetry注解来启用重试功能。SpringBootApplication EnableRetry // 关键启用Spring Retry public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }注意EnableRetry是通过Import引入了RetryConfiguration配置它会自动配置一个RetryOperationsInterceptor并利用Spring AOP为标记了Retryable的Bean创建代理。确保你的Retryable方法所在的Bean是被Spring容器管理的即使用了Component,Service等注解。3.2 基础使用模式与代码示例让我们通过一个模拟调用不稳定第三方服务的场景来演示。假设我们有一个PaymentService它需要调用一个外部的BankService来完成扣款操作。1. 定义服务与重试逻辑Service public class PaymentService { Autowired private BankService bankService; /** * 支付方法调用银行服务扣款失败时重试 */ Retryable( value {RemoteAccessException.class, TimeoutException.class}, // 针对网络和超时异常重试 maxAttempts 3, // 最多尝试3次1次初始2次重试 backoff Backoff(delay 1000, multiplier 2.0) // 延迟1秒开始下次延迟翻倍 ) public PaymentResult pay(Order order) { log.info(尝试支付订单: {}, 尝试次数: {}, order.getId(), getCurrentRetryCount()); // 模拟调用不稳定的银行服务 return bankService.debit(order.getAmount(), order.getAccount()); } /** * 重试全部失败后的降级处理 * 规则返回值类型与Retryable方法相同(PaymentResult)第一个参数为Throwable其余参数与原方法匹配(Order order) */ Recover public PaymentResult recoverPay(RemoteAccessException e, Order order) { log.error(支付服务彻底失败订单号: {} 异常: {}, order.getId(), e.getMessage()); // 优雅降级记录失败订单人工处理并返回一个明确的失败结果给用户 return PaymentResult.fail(支付系统繁忙请稍后再试或联系客服。订单已记录编号 order.getId()); } // 一个辅助方法用于演示获取当前重试上下文需要注入RetryContext // 实际使用中可以通过RetrySynchronizationManager获取 private int getCurrentRetryCount() { // 简化示例实际逻辑略 return 0; } } // 模拟不稳定的银行服务 Service class BankService { private int invokeCount 0; public PaymentResult debit(BigDecimal amount, String account) { invokeCount; // 模拟前两次调用都超时第三次成功 if (invokeCount 3) { throw new RemoteAccessException(银行服务连接超时); } return PaymentResult.success(支付成功交易号: TXN System.currentTimeMillis()); } }2. 测试与观察当你调用paymentService.pay(order)时控制台日志可能会如下所示尝试支付订单: ORDER-001 尝试次数: 1 尝试支付订单: ORDER-001 尝试次数: 2 尝试支付订单: ORDER-001 尝试次数: 3最终在第三次调用时成功。如果我们将BankService的模拟失败次数改为大于3则会看到在第三次重试失败后recoverPay方法被调用返回了降级结果。3.3 高级配置与自定义策略除了在注解上直接配置Spring Retry还支持通过RetryTemplate和RetryPolicy、BackOffPolicy进行更灵活的程序化配置。这在需要动态调整策略或与复杂条件结合时非常有用。1. 配置全局重试模板你可以定义一个RetryTemplateBean在多个地方复用。Configuration public class RetryConfig { Bean public RetryTemplate myRetryTemplate() { RetryTemplate template new RetryTemplate(); // 1. 定义重试策略什么情况下重试 SimpleRetryPolicy retryPolicy new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(4); // 最多4次尝试 // 2. 定义退避策略每次重试间隔多久 ExponentialBackOffPolicy backOffPolicy new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(2000L); // 初始间隔2秒 backOffPolicy.setMultiplier(1.5); // 倍数1.5 backOffPolicy.setMaxInterval(10000L); // 最大间隔10秒 template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); // 3. 注册监听器可选 template.registerListener(new MyRetryListener()); return template; } } // 自定义重试监听器 class MyRetryListener implements RetryListener { Override public T, E extends Throwable boolean open(RetryContext context, RetryCallbackT, E callback) { System.out.println(重试开始目标方法: callback); return true; // 返回true继续重试流程 } Override public T, E extends Throwable void close(RetryContext context, RetryCallbackT, E callback, Throwable throwable) { System.out.println(重试结束状态: context.isExhaustedOnly()); } Override public T, E extends Throwable void onError(RetryContext context, RetryCallbackT, E callback, Throwable throwable) { System.out.println(第 context.getRetryCount() 次重试失败异常: throwable.getMessage()); } }2. 在代码中使用RetryTemplateService public class AnotherService { Autowired private RetryTemplate myRetryTemplate; Autowired private UnstableApiClient apiClient; public String fetchDataWithTemplate(String param) { return myRetryTemplate.execute(context - { // 这里的逻辑会被重试 System.out.println(执行调用当前重试次数: context.getRetryCount()); return apiClient.call(param); }, context - { // 这里是RecoveryCallback相当于Recover System.out.println(所有重试失败执行降级); return Degraded Data for param: param; }); } }3. 注解与Template结合使用你甚至可以在Retryable注解中引用自定义的RetryTemplateBeanService public class HybridService { // 通过retryTemplate属性指定Bean名称 Retryable(retryTemplate myRetryTemplate) public void hybridMethod() { // ... } }4. 常见问题、避坑指南与排查技巧在实际使用Retryable和Recover时会遇到一些典型的“坑”。下面是我在多个项目中总结出来的经验。4.1 失效场景与原因分析问题1Retryable注解不生效方法调用一次失败就抛异常。原因A未启用AOP代理。Retryable基于Spring AOP实现。如果调用Retryable方法的地方是在同一个类内部的其他方法中即自调用由于Spring AOP使用代理机制自调用会绕过代理导致注解失效。解决方案将需要重试的方法抽取到另一个Bean中或者使用AopContext.currentProxy()来获取当前代理对象进行调用需在配置中开启exposeProxy true不推荐破坏代码结构。原因B异常未被正确捕获。抛出的异常类型不在Retryable(value {...})指定的范围内或者被exclude排除了。排查仔细检查抛出异常的实际类型。注意包装异常如FeignException内部包裹的异常。原因C方法不是public的。Spring AOP默认只对public方法进行代理。private、protected、default包内可见方法上的Retryable不会生效。解决方案确保方法为public。原因D未添加EnableRetry注解。解决方案检查主应用类或配置类上是否添加了EnableRetry。问题2Recover方法没有被调用。原因A方法签名不匹配。这是最常见的原因。请严格对照前文提到的Recover方法规则返回值类型、第一个Throwable参数、其余参数的类型和顺序。排查技巧可以写一个测试故意让重试失败然后调试查看最终抛出的异常类型是否与你Recover方法第一个参数类型匹配。注意如果Retryable方法可能抛出多种异常你需要为每种异常或它们的公共父类分别定义Recover方法。原因BRecover方法不在同一个Bean中。解决方案确保Recover方法与对应的Retryable方法在同一个Component、Service等注解标记的类中。原因C重试过程被中断。如果在重试过程中发生了非指定异常如Error或者重试监听器中open方法返回了false重试流程会立即终止不会进入Recover流程。4.2 性能与幂等性考量1. 重试的副作用非幂等操作警告这是使用重试机制最需要警惕的一点重试意味着同一个操作可能会被执行多次。如果你的方法是非幂等的即多次执行会产生不同的结果或副作用重试将导致严重问题。反面案例一个创建订单的方法。如果因为网络超时重试了3次可能会在数据库里创建出3个一模一样的订单。解决方案设计幂等接口这是根本解决方案。为操作设计唯一的业务ID如订单号、流水号在服务端实现“同一ID多次请求效果等同于一次请求”的逻辑。谨慎选择重试方法只为查询类、幂等更新类如根据状态机更新状态的方法添加重试。对于创建、删除等非幂等操作要么不重试要么必须结合幂等令牌等机制。使用状态检查在重试前先检查上一次操作是否已成功例如通过业务ID查询状态。2. 退避策略与系统压力不合理的重试策略如无延迟或固定短延迟会在下游服务短暂故障时引发“重试风暴”加剧其压力导致故障雪崩。最佳实践务必使用指数退避multiplier 1并加上随机抖动random true。这能有效分散重试请求的时间点。参数建议对于外部HTTP API调用初始延迟(delay)可以设置在1-3秒倍数(multiplier)设为2并启用随机因子。3. 超时设置重试会增加整体的请求耗时。你需要确保你的业务超时时间 (方法单次超时时间) * (最大尝试次数) 所有退避延迟的总和。否则可能在重试还没完成时调用方就因为超时而放弃了。4.3 与Spring生态的集成注意事项1. 与Transactional注解共用Retryable和Transactional都是基于AOP的。它们的执行顺序很重要。通常你需要让重试发生在一个事务内部还是让整个重试过程包含多次尝试被一个事务包裹常见模式将Retryable放在Transactional的外层。这样每次重试都会开启一个新的事务。如果某次重试成功了事务就提交如果所有重试都失败则最后一次尝试的事务会回滚除非被Recover捕获并处理。这需要将Retryable注解放在调用Transactional方法的外层方法上。2. 在异步方法Async上使用理论上可以但需要确保EnableRetry和EnableAsync都正确配置并且理解重试是在哪个线程上下文中进行的。异步任务的重试可能会在新线程中执行需要注意线程上下文信息如ThreadLocal的传递问题。3. 在Spring Cloud项目中在微服务调用中如使用Feign或RestTemplate重试可以发生在两个层面HTTP客户端层重试例如配置Feign或OkHttp的重试。这通常用于处理低级的网络连接错误。业务方法层重试即使用Retryable。这用于处理业务逻辑上的失败如返回了特定的错误码“系统繁忙请重试”。最佳实践是分层处理在HTTP客户端配置少量快速重试如1-2次处理网络抖动在业务方法层配置带有退避策略的重试处理业务性失败。避免重复重试导致请求指数级放大。4.4 监控与调试当重试逻辑复杂后监控变得至关重要。利用RetryListener如前文示例通过实现RetryListener接口你可以在重试开始、每次失败、重试结束时打入详细的日志或发送Metrics到监控系统如Prometheus。记录重试次数、异常信息、方法签名等这对于排查生产环境问题极具价值。区分日志级别在Retryable方法内使用DEBUG级别日志记录每次尝试在Recover方法内使用WARN或ERROR级别记录最终失败和降级情况便于告警。使用RetryContext在重试回调中可以通过RetrySynchronizationManager.getContext()获取当前重试的上下文里面包含了重试次数、上次异常等信息可以将其放入MDCMapped Diagnostic Context中方便在日志中追踪一次完整重试流程的所有日志。5. 进阶应用场景与模式扩展掌握了基础用法和避坑技巧后我们可以探索一些更高级的应用模式让重试机制更好地服务于复杂的业务场景。5.1 基于状态码或复杂条件的重试Retryable默认基于异常类型触发。但有时我们需要根据方法的返回值比如HTTP状态码、特定的错误码来决定是否重试。这时我们可以实现自定义的RetryPolicy。例如一个调用第三方API的方法返回一个ApiResponse对象当response.getCode()等于500或429Too Many Requests时我们需要重试。Component public class ApiResponseRetryPolicy extends SimpleRetryPolicy { Override public boolean canRetry(RetryContext context) { // 先调用父类逻辑判断次数等是否允许重试 if (!super.canRetry(context)) { return false; } // 获取上一次抛出的异常 Throwable lastThrowable context.getLastThrowable(); if (lastThrowable instanceof ApiException) { ApiException apiEx (ApiException) lastThrowable; String errorCode apiEx.getErrorCode(); // 只有特定错误码才重试 return 500.equals(errorCode) || 429.equals(errorCode) || 408.equals(errorCode); } // 如果不是ApiException或者不是指定错误码则不再重试 return false; } } // 在配置中应用自定义策略 Bean public RetryTemplate apiRetryTemplate() { RetryTemplate template new RetryTemplate(); template.setRetryPolicy(new ApiResponseRetryPolicy()); template.setBackOffPolicy(new ExponentialBackOffPolicy()); return template; } // 在服务中使用 Service public class ApiClientService { Retryable(retryTemplate apiRetryTemplate) public ApiResponse callExternalApi() { // ... 调用可能返回特定错误码的API } }5.2 组合使用与熔断器模式在微服务架构中重试通常与熔断器Circuit Breaker如Resilience4j或Spring Cloud Circuit Breaker结合使用形成更强大的弹性模式。熔断器当失败率达到阈值时快速失败直接调用降级方法避免持续重试拖垮系统并给下游服务恢复的时间。重试在熔断器处于“半开”状态或对于偶发性失败进行有限次数的重试。典型工作流请求到来先经过熔断器。如果熔断器处于“打开”状态直接调用降级方法快速返回。如果熔断器处于“关闭”或“半开”状态允许请求通过。请求进入业务方法该方法被Retryable修饰。如果业务方法调用失败Retryable逻辑启动进行有限次重试。如果重试成功结果返回熔断器记录成功。如果重试全部失败执行Recover降级熔断器记录一次失败。当失败积累到阈值熔断器“跳闸”进入打开状态。这种组合确保了系统在遇到持续故障时能快速熔断保护自己同时在遇到临时性故障时又能通过重试自我恢复。5.3 动态配置重试参数在有些场景下我们希望重试参数如重试次数、延迟时间能够根据不同的环境、不同的下游服务进行动态调整而不是硬编码在注解里。我们可以结合Spring的配置中心如Spring Cloud Config、Apollo、Nacos来实现。思路将重试参数配置在application.yml或配置中心然后通过ConfigurationProperties注入最后在Bean方法中动态构造RetryTemplate。# application.yml retry: policies: payment-service: max-attempts: 5 initial-interval-ms: 2000 multiplier: 1.5 max-interval-ms: 15000 sms-service: max-attempts: 3 initial-interval-ms: 1000 multiplier: 2.0ConfigurationProperties(prefix retry.policies.payment-service) Data public class PaymentRetryProperties { private int maxAttempts; private long initialIntervalMs; private double multiplier; private long maxIntervalMs; } Configuration EnableConfigurationProperties(PaymentRetryProperties.class) public class DynamicRetryConfig { Bean(name paymentRetryTemplate) public RetryTemplate paymentRetryTemplate(PaymentRetryProperties properties) { RetryTemplate template new RetryTemplate(); SimpleRetryPolicy policy new SimpleRetryPolicy(); policy.setMaxAttempts(properties.getMaxAttempts()); ExponentialBackOffPolicy backOffPolicy new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(properties.getInitialIntervalMs()); backOffPolicy.setMultiplier(properties.getMultiplier()); backOffPolicy.setMaxInterval(properties.getMaxIntervalMs()); template.setRetryPolicy(policy); template.setBackOffPolicy(backOffPolicy); return template; } } // 在Service中引用特定名称的Bean Service public class DynamicPaymentService { Retryable(retryTemplate paymentRetryTemplate) public void processPayment() { // ... } }通过这种方式运维人员可以在不重启应用的情况下通过修改配置中心的值来动态调整重试策略以适应不同的系统负载和下游服务状态。