接口调用失败后是否应该立刻返回错误,往往取决于故障类型。像网络抖动、远程服务短时超时、数据库乐观锁冲突这类瞬态问题,很多时候重试一次或几次就能恢复。Spring Retry 提供了声明式和编程式两套方案,既能快速接入,也能按业务精细控制。下面按“怎么接入、怎么配置、适合什么场景”来拆开说明,便于你直接对照项目选择实现方式。
先完成接入:依赖与开关配置
Spring Retry 的基础接入分两步:先引入依赖,再开启重试功能。原文给出的依赖是 spring-retry 和 aspectjwea ver,其中前者负责重试能力,后者用于 AOP 支持声明式注解。
1. 在 pom.xml 中加入依赖
org.springframework.retry
spring-retry
org.aspectj
aspectjwea ver
2. 在启动类上开启重试
接着,在 Spring Boot 启动类上增加 @EnableRetry。这一步的作用是让 Spring 为重试相关注解准备拦截能力,后面的 @Retryable 才能生效。
@SpringBootApplication
@EnableRetry
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class, args);
}
}
注解式重试怎么写:@Retryable 与 @Recover
如果你的目标是尽快给某个方法补上重试机制,注解式是最直接的方式。核心是 @Retryable,它可以声明“哪些异常要重试、最多重试几次、每次间隔多久”。
@Retryable 的典型配置项
下面这个退款示例里,遇到 RemoteAccessException 或 TimeoutException 时,会自动重试,最大尝试次数为 3 次,首次等待 1 秒,之后按倍数递增。
@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
@Retryable(
value = {RemoteAccessException.class, TimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public boolean refund(String orderNo) {
log.info("退款请求: {}", orderNo);
// 调用第三方支付接口
return thirdPartyRefund(orderNo);
}
@Recover
public boolean recover(RemoteAccessException e, String orderNo) {
log.error("退款失败,记录到异常表: {}", orderNo, e);
// 记录到失败表,人工处理
return false;
}
}
按这段配置,重试节奏可以理解为:首次执行失败后等待 1 秒,再次失败后间隔继续放大。原文描述中提到“每次重试间隔翻倍(2 秒、4 秒……)”,重点在于它使用的是指数退避思路,而不是固定间隔不断重放请求。
@Recover 负责最终兜底
当所有重试都失败后,Spring Retry 会转入 @Recover 标注的方法。这个回调通常用来做三件事:记录异常、发出告警、把任务写入人工处理队列或失败表。
这里有一个容易踩坑的点:@Recover 方法必须和对应的重试方法参数匹配,否则 Spring 无法正确关联。也就是说,除了异常参数外,业务参数也要保持一致。
需要更灵活时,用 RetryTemplate 自定义策略
注解式适合规则相对固定的方法;如果你需要更复杂的间隔控制、动态判断重试行为,通常就要转向 RetryTemplate。它本质上是把“重试次数、退避策略、异常判断”拆成可编排的 Bean 配置。
配置指数退避与最大重试次数
下面这段配置定义了一个自定义 RetryTemplate:初始间隔 1 秒,每次翻倍,最大间隔 10 秒,最多重试 5 次。
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
// 重试策略:最多重试5次,间隔递增
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000);
backOff.setMultiplier(2);
backOff.setMaxInterval(10000);
template.setBackOffPolicy(backOff);
// 异常判断:哪些异常需要重试
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
template.setRetryPolicy(retryPolicy);
return template;
}
}
这类写法的好处是,退避时间和重试次数都可以集中定义,便于在多个业务模块中复用同一套策略。
编程式重试适合哪些业务
当是否重试不只是“看异常类型”这么简单,而是要结合订单状态、上下文数据甚至外部返回结果动态判断时,编程式重试更合适。它把执行体和最终兜底逻辑都显式写在代码里,可控性更高。
RetryTemplate.execute 的两个回调
@Service
public class OrderService {
@Autowired
private RetryTemplate retryTemplate;
public boolean processOrder(Long orderId) {
return retryTemplate.execute(context -> {
log.info("处理订单,第{}次尝试", context.getRetryCount() + 1);
return doProcess(orderId);
}, context -> {
log.error("最终失败", context.getLastThrowable());
return false;
});
}
}
execute 接收两个回调:第一个是重试执行体,第二个是重试耗尽后的恢复逻辑。示例里的 context.getRetryCount() 还能直接拿到当前重试次数,适合写日志、打点或做链路跟踪。
注解式和编程式怎么选
如果某个方法的重试条件固定、异常类型明确,用 @Retryable 更省事;如果你需要把重试过程纳入业务流程控制,例如按订单状态、库存状态、调用结果动态决定是否继续,则 RetryTemplate 更稳妥。
全局配置与秒杀场景中的落地方式
除了方法级配置,Spring Retry 还可以在配置文件里做一定程度的统一控制。不过要注意,这种配置并不会无条件覆盖你在注解上写死的参数。
application.yml 中的重试配置
spring:
retry:
max-attempts: 3 # 全局最大重试次数
原文特别提醒了一点:这个配置只对 RetryTemplate 默认行为有效;如果你在 @Retryable 里已经指定了 maxAttempts,那方法上的配置优先级更高。
秒杀库存扣减为什么适合重试
秒杀系统里,库存扣减常常配合乐观锁来防止超卖。问题在于,乐观锁失败未必代表业务彻底失败,很多时候只是高并发下的瞬时冲突。这类场景就很适合短间隔、低次数的自动重试。
@Service
public class SeckillService {
@Retryable(
value = OptimisticLockException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 200)
)
public boolean deductStock(Long productId) {
// 乐观锁失败时自动重试
return productMapper.updateStock(productId) > 0;
}
}
这个例子中,遇到 OptimisticLockException 后会在 200 毫秒后重试,最多 3 次。对于高并发库存扣减,它往往比“立即报错”更能提升成功率,同时又不会把等待时间拉得过长。
最后的边界:哪些接口不该随便重试
重试机制适合处理瞬态故障,但不意味着所有失败都值得重放。比如幂等性不足的接口,如果重试后产生重复扣款、重复下单、重复写入,后果往往比一次失败更严重。
因此,真正落地时至少要同时考虑三件事:异常是否是瞬态的、接口是否具备幂等性、重试次数和间隔是否会放大系统压力。把这三个判断做清楚,Spring Retry 才能真正用得优雅。










