Spring中@Retryable与@Transactional注解结合使用失效问题排查求助
我帮你梳理下这个重试逻辑失效还陷入无限循环的问题,大概率是这几个常见原因导致的,咱们逐个排查解决:
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

