Spring Boot Retry与Transactional结合时重试未按预期生效问题
问题场景
代码结构
Kafka监听器组件
@KafkaListener public void method(){ service1.method1(); }
服务1
@Transactional public void method1(){ service2.method2(); }
服务2
@Transactional @Retry(Exception.class,3) public void method2(){ // 业务逻辑,可能抛出OptimisticLockException some logic ,may produce an exception }
预期与实际偏差
- 预期:
method2抛出异常时触发Spring Retry重试3次 - 实际:重试未执行,直接抛出Kafka顶层异常:
org.springframework.kafka.KafkaException: Seek to current after exception; nested exception is org.springframework.kafka.listener.ListenerExecutionFailedException: Listener method 'public void iesd.devops.monitoring.consumer.qualitygates.QualityGateTestExecutionEventsConsumer.testExecutionEventsListener(org.apache.kafka.clients.consumer.ConsumerRecord<java.lang.String, iesd.devops.monitoring.entity.qualitygates.TestExecutionEvent>)' threw exception; nested exception is javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [iesd.devops.monitoring.entity.qualitygates.gates.testgates.TestGatePayload#1]; nested exception is javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [iesd.devops.monitoring.entity.qualitygates.gates.testgates.TestGatePayload#1]
at org.springframework.kafka.listener.SeekUtils.seekOrRecover(SeekUtils.java:208) ~[spring-kafka-2.9.0.jar:2.9.0]
at org.springframework.kafka.listener.DefaultErrorHandler.handleRemaining(DefaultErrorHandler.java:169) ~[spring-kafka-2.9.0.jar:2.9.0]
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeErrorHandler(KafkaMessageListenerContainer.java:2817) ~[spring-kafka-2.9.0.jar:2.9.0]
核心原因
- Retry注解拼写错误:Spring Retry的官方注解是
@Retryable,而非代码中的@Retry,这直接导致重试逻辑根本不生效。 - 事务绑定冲突:
service1.method1的事务与service2.method2的事务默认以REQUIRED传播行为绑定,当method2抛出异常,上层事务被标记为回滚状态,无法在同一个事务上下文内触发重试,异常直接冒泡到Kafka监听器。 - Kafka错误处理器拦截:Kafka监听器的
DefaultErrorHandler会捕获所有未处理的异常,执行seek操作并封装为KafkaException抛出,阻断了Retry逻辑的执行流程。
解决步骤
1. 修正Retry注解并启用Retry
- 将
@Retry替换为@Retryable,并指定重试参数:@Transactional @Retryable(value = OptimisticLockException.class, maxAttempts = 3) public void method2(){ some logic ,may produce an exception } - 在Spring Boot启动类添加
@EnableRetry注解,开启Retry功能:@SpringBootApplication @EnableRetry public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2. 调整事务传播行为,隔离重试事务
修改service2.method2的事务传播行为为REQUIRES_NEW,确保每次重试都开启独立事务,避免与上层事务绑定导致的回滚冲突:
@Transactional(propagation = Propagation.REQUIRES_NEW) @Retryable(value = OptimisticLockException.class, maxAttempts = 3) public void method2(){ some logic ,may produce an exception }
3. 配置Kafka错误处理器,放行目标异常
自定义Kafka的DefaultErrorHandler,让OptimisticLockException不被Kafka拦截,交由Retry逻辑处理:
@Bean public DefaultErrorHandler kafkaErrorHandler() { DefaultErrorHandler errorHandler = new DefaultErrorHandler(); // 允许OptimisticLockException冒泡到Retry逻辑 errorHandler.addNotRetryableExceptions(OptimisticLockException.class); return errorHandler; }
4. 可选:将Retry逻辑上移到事务外
如果需要更彻底的隔离,可将@Retryable移到Kafka监听器方法或service1.method1上,确保重试在事务启动前触发:
@KafkaListener @Retryable(value = OptimisticLockException.class, maxAttempts = 3) public void method(){ service1.method1(); }
内容的提问来源于stack exchange,提问作者Abhiram Reddy

