在基于 Spring 框架进行开发时,@Transactional 注解是管理数据库事务的利器。但你是否遇到过明明添加了注解,事务却没有按预期回滚的情况?本文将深入探讨三种最常见的 @Transactional 失效场景,并结合 Spring 源码分析其底层原理,助你彻底避开这些“坑”。
一、@Transactional失效场景分析
1. 方法修饰符为非 public
核心问题:当 @Transactional 注解标注的方法修饰符为非 public(如 default、protected、private)时,注解将会完全失效。
下面是一个典型的错误示例,@Transactional 注解修饰了一个默认访问符的方法:
@Component
public class TestServiceImpl {
@Resource
TestMapper testMapper;
@Transactional
void insertTestWrongModifier() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}
在同一包内创建调用类:
@Component
public class InvokcationService {
@Resource
private TestServiceImpl testService;
public void invokeInsertTestWrongModifier(){
//调用@Transactional标注的默认访问符方法
testService.insertTestWrongModifier();
}
}
测试用例:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
InvokcationService invokcationService;
@Test
public void testInvoke(){
invokcationService.invokeInsertTestWrongModifier();
}
}
在这种调用方式下,事务将不会开启。即使方法内抛出了 NeedToInterceptException,第一条 testMapper.insert(new Test(10,20,30)) 操作也不会回滚。如果将 TestServiceImpl#insertTestWrongModifier 方法改为 public,事务便会正常开启并回滚。
2. 类内部方法调用
核心问题:在同一个类的内部,通过 this 调用另一个被 @Transactional 标注的方法,事务同样不会生效。
定义一个包含内部调用的服务类:
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestInnerInvoke() {
//正常public修饰符的事务方法
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
public void testInnerInvoke(){
//类内部调用@Transactional标注的方法。
insertTestInnerInvoke();
}
}
编写测试代码:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
TestServiceImpl testService;
/**
* 测试内部调用@Transactional标注方法
*/
@Test
public void testInnerInvoke(){
//测试外部调用事务方法是否正常
//testService.insertTestInnerInvoke();
//测试内部调用事务方法是否正常
testService.testInnerInvoke();
}
}
运行测试会发现:外部直接调用 insertTestInnerInvoke() 方法可以正常开启并回滚事务;而通过内部方法 testInnerInvoke() 间接调用时,事务不会开启,数据操作无法回滚。
3. 异常在方法内部被捕获
核心问题:事务方法内部捕获了异常,但没有将异常重新抛出,导致事务代理逻辑无法感知到异常,从而不会触发回滚。
看以下代码:
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestCatchException() {
try {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
//运行期间抛异常
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}catch (Exception e){
System.out.println(“i catch exception”);
// 异常在此被“吞掉”,没有继续抛出
}
}
}
测试调用:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
TestServiceImpl testService;
@Test
public void testCatchException(){
testService.insertTestCatchException();
}
}
运行结果:虽然 NeedToInterceptException 被抛出,但它在方法内部被捕获并处理,没有传播到方法之外。因此,testMapper.insert(new Test(210,20,30)) 这条操作不会被回滚。
了解现象后,你是否好奇 Spring 底层是如何处理事务的?为什么这些场景下注解会失灵?接下来,我们结合源码一探究竟。如果你对 Spring 的 动态代理 机制感兴趣,可以深入探索其实现原理。
二、@Transactional失效原理深度解析
Spring 的声明式事务管理本质上是通过 AOP(面向切面编程) 和动态代理实现的。理解这一点是分析所有失效场景的关键。
场景1原理:非public方法为何失效?
Spring 在初始化 Bean 时,会扫描 @Transactional 注解信息,并为符合条件的 Bean 创建代理对象。扫描逻辑中有一个关键判断:如果方法的修饰符不是 public,则其 @Transactional 注解信息会被忽略。
相关源码位于 AbstractFallbackTransactionAttributeSource#computeTransactionAttribute 方法中:
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don‘t allow no-public methods as required.
//非public 方法,返回@Transactional信息一律是null
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
//后面省略.......
}
这导致了两种结果:
-
不创建代理对象:如果一个类的所有方法都是非 public 且标注了 @Transactional,Spring 会认为没有需要代理的事务方法,因此根本不会为该 Bean 创建代理对象。你注入的就是原始对象,自然没有事务功能。
-
不进行代理调用:如果类中同时存在 public 和非 public 的 @Transactional 方法,Spring 会因为 public 方法而创建代理对象。但当调用非 public 方法时,代理逻辑在查找该方法对应的拦截器(即事务增强逻辑)时,会再次调用 computeTransactionAttribute。由于方法非 public,返回的 TransactionAttribute 为 null,代理逻辑便认为该方法不需要事务管理,于是直接调用原始方法,绕过了事务。
图示对比:
下图展示了正常 public 方法下,Service 被 CGLIB 创建的代理对象所包裹,内含多个拦截器(包括事务拦截器)。

而非代理对象则直接指向原始的 MapperProxy。

场景2原理:内部调用为何失效?
原因非常直接:事务管理通过代理对象生效。
当从类外部调用 testService.insertTestInnerInvoke() 时,实际调用的是代理对象的方法,代理对象中的事务拦截器会先开启事务,再调用目标方法。
而当在类内部通过 this.insertTestInnerInvoke() 调用时,this 指向的是目标对象本身,而非其代理对象。这次调用完全绕过了代理机制,相当于直接执行普通方法,事务拦截器根本没有介入的机会。
有一种“炫技”式的解决方案:在类内部注入自身的代理对象(如 @Resource TestServiceImpl testServiceImpl),然后通过 testServiceImpl.insertTestInnerInvoke() 调用。但这在实际开发中并不推荐,它破坏了代码的清晰度。
场景3原理:吞掉异常为何不回滚?
事务的回滚逻辑是由代理对象中的异常处理机制触发的。核心逻辑位于 TransactionAspectSupport#invokeWithinTransaction 方法中:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// ... 省略部分代码
//开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
//反射调用业务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
//异常时,在catch逻辑中回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex; // !!! 关键:将异常继续抛出
}
finally {
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
从代码可以清晰看到,只有在 catch 块中捕获到业务方法抛出的异常,才会执行 completeTransactionAfterThrowing(txInfo, ex) 进行回滚。回滚后,异常还被重新抛出 (throw ex)。
如果在你的业务方法中(如 insertTestCatchException)使用 try-catch 将异常捕获并处理掉,没有重新抛出,那么代理层的 catch 块就根本不会执行。代理逻辑会认为方法执行成功,从而执行 commitTransactionAfterReturning(txInfo) 提交事务,导致数据被持久化。
总结与避坑指南
- 坚持public修饰符:确保所有
@Transactional 注解标注的方法都是 public 的。
- 避免内部调用:不要直接在同一个类的非事务方法中调用事务方法。如果需要,可考虑将事务方法抽取到另一个 Service 中,或使用
AopContext.currentProxy() 获取代理对象(需额外配置)进行调用。
- 谨慎处理异常:在事务方法中,如果捕获了异常且不希望触发回滚,可以手动回滚或使用
@Transactional(noRollbackFor = Exception.class)。如果希望异常触发回滚,务必将其抛出或转换为 RuntimeException 抛出。
理解 Spring 事务的代理本质,是解决一切诡异事务问题的钥匙。希望本文分析的三个场景和底层原理,能帮助你在日常开发中更得心应手地驾驭 Spring 事务管理,写出更健壮的代码。在 云栈社区 上,你还可以找到更多关于数据库事务隔离级别、传播行为等深度讨论,欢迎一起交流学习。
本文内容来源于技术博客,由云栈社区编辑整理,旨在分享实用的技术排查思路。文中涉及的观点与代码仅供学习参考。

