找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1989

积分

0

好友

263

主题
发表于 3 天前 | 查看: 17| 回复: 0

在基于 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;
   }
   //后面省略.......
 }

这导致了两种结果:

  1. 不创建代理对象:如果一个类的所有方法都是非 public 且标注了 @Transactional,Spring 会认为没有需要代理的事务方法,因此根本不会为该 Bean 创建代理对象。你注入的就是原始对象,自然没有事务功能。

  2. 不进行代理调用:如果类中同时存在 public 和非 public@Transactional 方法,Spring 会因为 public 方法而创建代理对象。但当调用非 public 方法时,代理逻辑在查找该方法对应的拦截器(即事务增强逻辑)时,会再次调用 computeTransactionAttribute。由于方法非 public,返回的 TransactionAttributenull,代理逻辑便认为该方法不需要事务管理,于是直接调用原始方法,绕过了事务。

图示对比
下图展示了正常 public 方法下,Service 被 CGLIB 创建的代理对象所包裹,内含多个拦截器(包括事务拦截器)。
CGLIB动态代理对象内部回调接口列表

而非代理对象则直接指向原始的 MapperProxy。
非代理对象直接持有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) 提交事务,导致数据被持久化。

总结与避坑指南

  1. 坚持public修饰符:确保所有 @Transactional 注解标注的方法都是 public 的。
  2. 避免内部调用:不要直接在同一个类的非事务方法中调用事务方法。如果需要,可考虑将事务方法抽取到另一个 Service 中,或使用 AopContext.currentProxy() 获取代理对象(需额外配置)进行调用。
  3. 谨慎处理异常:在事务方法中,如果捕获了异常且不希望触发回滚,可以手动回滚或使用 @Transactional(noRollbackFor = Exception.class)。如果希望异常触发回滚,务必将其抛出或转换为 RuntimeException 抛出。

理解 Spring 事务的代理本质,是解决一切诡异事务问题的钥匙。希望本文分析的三个场景和底层原理,能帮助你在日常开发中更得心应手地驾驭 Spring 事务管理,写出更健壮的代码。在 云栈社区 上,你还可以找到更多关于数据库事务隔离级别、传播行为等深度讨论,欢迎一起交流学习。

本文内容来源于技术博客,由云栈社区编辑整理,旨在分享实用的技术排查思路。文中涉及的观点与代码仅供学习参考。
加载动画
装饰星形图案




上一篇:Spring @Transactional使用不当引发长事务:记一次报销系统生产故障分析与解决方案
下一篇:Spring事务管理三剑客:@Transactional、TransactionTemplate与TransactionManager实战选型
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 10:06 , Processed in 0.561792 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表