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

Spring中@Retryable与@Transactional注解结合使用失效问题排查求助

排查Spring Retry失效+无限循环问题

我帮你梳理下这个重试逻辑失效还陷入无限循环的问题,大概率是这几个常见原因导致的,咱们逐个排查解决:

1. 没有通过Spring代理对象调用方法(最可能的根因)

看你的测试代码,直接调用了updateClient(info)——如果这个调用是在测试类内部,或者你没有通过Spring容器注入的Service Bean来调用,而是自己new了一个Service实例,那Spring的AOP代理根本不会生效!毕竟@Retryable是靠Spring AOP实现的,只有通过代理对象调用方法时,重试逻辑才会被触发。

解决办法:
在测试类里正确注入你的Service Bean,通过代理对象调用方法:

@ExtendWith(SpringExtension.class)
// 别忘了把你的Service所在的配置类也加到上下文里
@ContextConfiguration(classes = {DSConfig.class, YourClientServiceConfig.class }) 
@ActiveProfiles("test")
@Slf4j
class ClientServiceRetryTest {

    @Autowired
    private ClientService clientService; // 注入Spring管理的Service Bean

    @Test
    void retryTest() {
        String info = "Test";
        clientService.updateClient(info); // 通过代理对象调用,触发重试逻辑
    }
}

2. @Transactional和@Retryable的代理顺序搞反了

当同一个方法上同时加了@Transactional和@Retryable,默认Spring会让事务切面的优先级更高。这就导致事务先执行,当抛出CannotAcquireLockException时,事务管理器可能先捕获异常并回滚,重试切面根本没机会感知到目标异常;更糟的是,默认的事务传播特性是REQUIRED,重试会在同一个事务里跑,锁一直被占着,自然会陷入循环。

解决办法:
调整切面顺序,让重试切面先于事务切面执行:

  • 给你的重试配置类加@Order,设置一个比事务切面更高的优先级(数值越小优先级越高,事务切面默认是最低优先级):
@Configuration
@EnableRetry
@Order(1) // 确保重试切面先执行
public class RetryConfig {
}

同时,给@Transactional明确指定回滚异常,确保异常能透传给重试切面:

@Retryable(value = CannotAcquireLockException.class, maxAttempts = 3, 
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@Transactional(rollbackFor = CannotAcquireLockException.class) // 明确异常回滚规则
public void updateClient(String info) throws Exception {
    updateClientFromDB(info);
}

3. 异常被包装了,重试注解没匹配到

有时候数据库的锁异常会被Spring事务管理器或者JDBC驱动包装成其他异常(比如TransactionSystemException),而不是直接抛出CannotAcquireLockException,这就导致@Retryable的异常匹配失效,自然不会触发重试,反而因为锁一直被占着陷入循环。

解决办法:
先在方法里打印完整异常栈,确认实际抛出的异常类型:

@Retryable(value = CannotAcquireLockException.class, maxAttempts = 3, 
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@Transactional
public void updateClient(String info) throws Exception {
    try {
        updateClientFromDB(info);
    } catch (Exception e) {
        log.error("更新客户信息失败", e); // 打印完整异常栈,看实际抛出的是什么异常
        throw e;
    }
}

如果发现异常被包装,修改@Retryable的配置,匹配包装后的异常或者根原因:

@Retryable(
    value = TransactionSystemException.class, 
    maxAttempts = 3,
    // 通过表达式匹配异常的根原因
    exceptionExpression = "#root.cause instanceof T(org.springframework.dao.CannotAcquireLockException)",
    backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000)
)
@Transactional(rollbackFor = Exception.class)
public void updateClient(String info) throws Exception {
    updateClientFromDB(info);
}

4. @EnableRetry的配置没被Spring上下文加载

你说在配置类上加了@EnableRetry,但要确认这个配置类已经被Spring扫描到。比如你的测试类@ContextConfiguration只指定了DSConfig.class,如果Retry的配置类不在这个配置的扫描范围内,那重试功能根本没启用。

解决办法:
把包含@EnableRetry的配置类加到@ContextConfiguration里:

@ContextConfiguration(classes = {DSConfig.class, RetryConfig.class })

或者在DSConfig里加上@ComponentScan,确保能扫描到Retry配置类。

5. 事务传播特性导致锁无法释放

如果重试时事务没有正确回滚,锁一直被持有,那每次重试都会触发锁异常,看起来就像无限循环(哪怕你设置了maxAttempts=3,也可能因为异常没被正确捕获而一直重试)。

解决办法:
让每次重试都在独立的事务里执行,修改@Transactional的传播特性为REQUIRES_NEW:

@Retryable(value = CannotAcquireLockException.class, maxAttempts = 3, 
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = CannotAcquireLockException.class)
public void updateClient(String info) throws Exception {
    updateClientFromDB(info);
}

这样每次重试都会开启新事务,前一次事务回滚后会释放锁,避免锁一直被占用。


内容的提问来源于stack exchange,提问作者Voav

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.01 02:32:30