You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot Retry与Transactional结合时重试未按预期生效问题

Spring Boot中@Transactional与Spring Retry结合重试失效问题排查与解决

问题场景

代码结构

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]

核心原因

  1. Retry注解拼写错误:Spring Retry的官方注解是@Retryable,而非代码中的@Retry,这直接导致重试逻辑根本不生效。
  2. 事务绑定冲突:service1.method1的事务与service2.method2的事务默认以REQUIRED传播行为绑定,当method2抛出异常,上层事务被标记为回滚状态,无法在同一个事务上下文内触发重试,异常直接冒泡到Kafka监听器。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 18:15:49