在企业级应用中,重试数据库操作是处理临时性故障(如死锁、瞬时连接问题或服务短暂中断)的常见需求。通过声明式注解(如 @Retryable 与 @Transactional)或完全编程式的方法(使用 RetryTemplate 与 TransactionTemplate),Spring框架为实现可靠的重试机制提供了支持。
无论采用哪种方式,其核心原则是 确保每次重试尝试都在其独立的事务中执行。如果在同一个事务内进行多次重试,早期尝试的异常可能会将事务标记为“仅回滚”,这将导致后续所有尝试立即失败,即使后续逻辑本身可以成功执行。
本文将深入解析在 Spring 中如何协调重试与事务,并分别演示基于注解和编程式两种实现方案,帮助你构建健壮的数据操作逻辑。
1. 项目依赖配置
要在你的 Spring Boot 项目中使用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-aop</artifactId>
</dependency>
2. 启用 Spring Retry 功能
通过在配置类上添加 @EnableRetry 注解来启用重试功能。其中,order 属性的设置至关重要,它决定了重试拦截器与事务拦截器的执行顺序。将其设置为最低优先级(Ordered.LOWEST_PRECEDENCE),可以确保重试建议包裹在事务建议之外,这是实现每次重试都运行在新事务中的关键。
@Configuration
@EnableRetry(order = Ordered.LOWEST_PRECEDENCE)
public class RetryConfig {
}
此配置确保了重试逻辑首先被触发。当异常发生并触发重试时,Spring会回滚当前事务,然后重新调用目标方法,并为这次新的尝试开启一个全新的事务。
3. 声明式注解方案:@Retryable 与 @Transactional
结合使用 @Retryable 和 @Transactional 注解是一种简洁的声明式方案。在这种模式下,重试机制包装了事务边界,从而保证每次重试都在一个干净、独立的事务上下文中执行。
@Service
public class PaymentService {
private static final Logger logger = Logger.getLogger(PaymentService.class.getName());
private final PaymentRepository paymentRepository;
private static int attempt = 1; // 用于演示的计数器
public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@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++;
throw new RuntimeException(“模拟瞬时故障”);
}
payment.setStatus(“SUCCESS”);
paymentRepository.save(payment);
logger.log(Level.INFO, “Payment processed successfully on attempt #: {0}”, attempt);
}
}
在这个示例中,方法前两次执行会故意抛出异常。由于重试建议被应用在事务建议之外,Spring会安全地回滚每次失败尝试对应的事务,然后触发一次新的方法调用,每次调用都始于一个新事务。当执行到第三次尝试时,前两次失败的“污点”已被清除,第三次尝试在一个全新的事务中成功执行。
4. 编程式方案:RetryTemplate 与 TransactionTemplate
对于需要更精细控制重试策略、异常分类和事务边界的场景,编程式方案提供了更高的灵活性。通过组合使用 RetryTemplate 和 TransactionTemplate,你可以完全掌控重试的时机、异常处理以及事务的执行。这种方案在构建复杂的 Java 后端服务时尤为有用。
@Service
public class PaymentServiceProgrammatic {
private static final Logger logger = Logger.getLogger(PaymentServiceProgrammatic.class.getName());
private final PaymentRepository paymentRepository;
private final TransactionTemplate transactionTemplate;
public PaymentServiceProgrammatic(PaymentRepository paymentRepository, TransactionTemplate transactionTemplate) {
this.paymentRepository = paymentRepository;
this.transactionTemplate = transactionTemplate;
}
private final 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) {
throw new RuntimeException(“模拟瞬时故障”);
}
payment.setStatus(“SUCCESS”);
paymentRepository.save(payment);
logger.info(“Payment processed successfully on retry attempt: ” + context.getRetryCount());
return null;
});
});
}
}
此示例中,RetryTemplate 负责自动检测失败并最多重试3次,同时提供对重试上下文(如当前尝试次数)的访问。嵌套在内的 TransactionTemplate 则确保每次重试都在一个新的、隔离的事务中执行,在失败时正确回滚所有更改,防止产生部分提交的结果,这对于保障类似 MySQL 等关系型数据库的数据一致性至关重要。
5. 总结
本文探讨了在Spring应用中实现可靠事务性重试的两种主要方式。声明式注解方案(@Retryable + @Transactional)简洁清晰,适合大多数常规场景;而编程式方案(RetryTemplate + TransactionTemplate)则提供了对事务边界、重试策略和退避机制的完全控制权,适用于复杂业务逻辑。无论选择哪种方案,核心都在于确保每次重试都在独立的新事务中执行,从而避免先前失败尝试的回滚副作用污染后续操作,最终保证业务处理的一致性与确定性。