
在企业级应用中,重试数据库操作是一项必要功能,特别是在处理临时性问题(如死锁、瞬时连接故障、竞争条件或短暂的服务中断)时。Spring 提供了使用声明式注解(如 @Retryable 与 @Transactional 结合)以及基于 RetryTemplate 和 TransactionTemplate 的完全编程式方法来实现可靠重试的机制。
这两种方法都必须确保每次重试都在自己的事务中执行,因为在单个事务内执行多次重试尝试可能会导致立即失败。这是因为早期的异常可能会将事务标记为仅回滚状态,导致所有剩余尝试都失败,即使它们本可以成功。
本文解释了 Spring Retry 如何与事务性方法一起工作,并演示了基于注解和编程式方法来实现可靠重试。
要使用 Spring Retry,请在 pom.xml 中包含必要的依赖项:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
使用 @EnableRetry 注解启用 Spring Retry。order 值确定重试拦截器相对于 Spring 事务拦截器的优先级。将其设置为最低优先级可确保重试建议包装事务建议,这对于将每次重试隔离到其自己的新事务中至关重要。
@Configuration
@EnableRetry(order = Ordered.LOWEST_PRECEDENCE)
public class RetryConfig {
}
此设置确保重试逻辑首先激活。当发生重试时,Spring 会退出前一个事务,再次调用方法,并为新尝试启动一个新事务。
实现重试的一种解决方案是将 Spring 的 AOP 支持的@Retryable注解与@Transactional注解相结合。在这种模式中,重试机制包装了事务边界,以便每次重试尝试都在一个干净、独立的事务中执行。
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 2000)
)
@Transactional
public void processPayment(double amount) {
logger.log(Level.INFO, "Attempt #: {0}", attempt);
Payment payment = new Payment("PENDING", amount);
paymentRepository.save(payment);
// 模拟前两次尝试的瞬时异常
if (attempt < 3) {
attempt++;
thrownew RuntimeException("模拟瞬时故障");
}
payment.setStatus("SUCCESS");
paymentRepository.save(payment);
logger.log(Level.INFO, "Payment processed successfully on attempt #: {0}", attempt);
}
在此示例中,前两次尝试通过抛出异常故意失败。由于重试建议在事务建议之外应用,Spring 会安全地回滚每次失败的尝试,然后触发新的调用,每次都开始一个新的事务。当方法到达第三次尝试时,早期尝试的失败标志不再影响它。第三次尝试在干净的事务内运行并成功进行。
某些用例需要对重试策略、错误分类和事务边界进行更精细的控制。在这种情况下,使用 RetryTemplate 和 TransactionTemplate 的编程式方法提供了对重试时机、异常处理和事务执行的更精细控制。
@Service
publicclass PaymentServiceProgrammatic {
privatestaticfinal Logger logger = Logger.getLogger(PaymentServiceProgrammatic.class.getName());
privatefinal PaymentRepository paymentRepository;
privatefinal TransactionTemplate transactionTemplate;
public PaymentServiceProgrammatic(PaymentRepository paymentRepository, TransactionTemplate transactionTemplate) {
this.paymentRepository = paymentRepository;
this.transactionTemplate = transactionTemplate;
}
privatefinal RetryTemplate retryTemplate = new RetryTemplateBuilder()
.maxAttempts(3)
.fixedBackoff(Duration.ofMillis(100))
.build();
public void processPayment(double amount) {
retryTemplate.execute(context -> {
logger.info("Retry attempt: " + context.getRetryCount());
// 在手动控制的事务内执行操作
return transactionTemplate.execute(status -> {
Payment payment = new Payment("PENDING", amount);
paymentRepository.save(payment);
// 演示模拟瞬时故障
if (context.getRetryCount() < 3) {
thrownew RuntimeException("模拟瞬时故障");
}
payment.setStatus("SUCCESS");
paymentRepository.save(payment);
logger.info("Payment processed successfully on retry attempt: " + context.getRetryCount());
returnnull;
});
});
}
}
此示例使用 RetryTemplate 自动检测故障,最多重试三次,固定退避 100ms,并提供对重试上下文的访问,包括当前尝试次数,而 TransactionTemplate确保每次重试都在新的、隔离的事务内执行,在失败时正确回滚,并防止任何部分提交的结果。
在本文中,我们探讨了如何使用声明式和编程式方法在 Spring 中实现可靠的事务性重试。我们演示了 @Retryable 与 @Transactional 结合如何实现干净、基于注解的重试,以及 RetryTemplate 与 TransactionTemplate 如何提供对事务边界、重试策略和退避策略的完全控制。通过确保每次重试在新事务内执行,这些策略可防止来自先前失败的回滚副作用,并保证一致、确定的结果。