@Transactional方法执行后抛RuntimeException致未提交的原因及解决
场景还原
非事务方法调用被@Transactional修饰的事务方法,事务方法执行完成后抛出RuntimeException,出现更新SQL已执行flush但未提交的异常情况。
代码结构
ValidateCodeService类:
public void callingMethod(){ // 业务逻辑 if (codeReceivedFromUser != codeStoredInDB){ codeDTO.setNoOfRetries(codeDTO.getNoOfRetries() - 1); codeRepository.update(codeDTO); throw new RuntimeException(CodeError.CDE1); } }
CodeRepository类:
public class CodeRepository extends SuperRepository<Code, Integer> { @Transactional public CodeDTO update(CodeDTO dto){ // 业务逻辑 super.merge(TransformDTOToEntity(dto)); return dto; } }
SuperRepository类:
public class SuperRepository<T> { public void merge(T entity){ getEm.merge(entity); getEm.flush(); } }
日志现象
抛异常场景日志:
hibernate flush
[my update query]
log error
[my CodeError.CDE1]
SQL已执行flush但未提交,直接触发报错。无异常场景日志:
hibernate flush
[my update query]
hibernate commit
JDBC commit
更新操作正常提交。
问题原因
Spring @Transactional的默认规则是:方法正常执行完毕时提交事务;若方法抛出未捕获的RuntimeException,则触发事务回滚。
核心在于事务传播机制:非事务方法callingMethod调用带@Transactional的update方法时,Spring会为update创建新事务。但update执行完成后事务尚未提交,上层callingMethod就抛出了RuntimeException——该异常会被Spring事务管理器捕获,触发事务回滚,因此即使执行了flush,最终也不会提交。
而无异常场景下,update执行完成后无异常抛出,Spring会正常提交事务,因此能看到commit日志。
解决方法:确保更新在抛异常前提交
要让update的事务独立于上层异常完成提交,可采用以下两种方式:
1. 修改事务传播级别(推荐)
将update方法的@Transactional设置为REQUIRES_NEW传播级别,强制创建独立新事务,该事务会在update执行完成后立即提交,不受上层方法异常影响:
public class CodeRepository extends SuperRepository<Code, Integer> { @Transactional(propagation = Propagation.REQUIRES_NEW) public CodeDTO update(CodeDTO dto){ // 原有业务逻辑 super.merge(TransformDTOToEntity(dto)); return dto; } }
REQUIRES_NEW的作用是:每次调用该方法都会开启新的独立事务,若当前存在其他事务则会被挂起;新事务执行完毕后立即提交,与上层事务或异常完全隔离。
2. 手动控制事务提交(不推荐)
若无法修改事务传播级别,可在update方法中手动提交事务,但这种方式会脱离Spring声明式事务管理,增加维护成本:
public class CodeRepository extends SuperRepository<Code, Integer> { @Transactional public CodeDTO update(CodeDTO dto){ // 原有业务逻辑 super.merge(TransformDTOToEntity(dto)); // 手动提交事务 getEm.getTransaction().commit(); return dto; } }
注意:手动提交后,后续事务操作需重新开启,易引发事务管理混乱,仅适用于特殊场景。
内容的提问来源于stack exchange,提问作者Kusai

