@Transactional事务失效总结

@Transactional 注解详解与代理对象问题解析

Spring 的 @Transactional 注解用于声明式事务管理,它通过 AOP 代理 在方法执行前后添加事务控制逻辑。正确理解其原理和常见陷阱是避免事务失效的关键。

1. 核心工作原理

  1. 代理生成 :Spring 容器启动时,会扫描 @Transactional 注解,为对应的 Bean 创建代理对象(JDK 动态代理或 CGLIB)。
  2. 方法拦截:当外部调用 Bean 的方法时,实际调用的是代理对象的方法。代理对象先执行事务相关操作(如开启事务、设置隔离级别),再调用目标对象的原方法,最后根据执行结果提交或回滚事务。
  3. 事务绑定 :事务与当前线程绑定(通过 ThreadLocal 存储事务资源),因此同一线程内的方法调用默认共享同一个事务。

2. 关键属性详解

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Transactional {
    // 事务管理器名称,用于指定使用哪个事务管理器(多数据源时常用)
    String value() default "";
    String transactionManager() default "";

    // 传播行为(默认 REQUIRED)
    Propagation propagation() default Propagation.REQUIRED;

    // 隔离级别(默认使用数据库默认隔离级别)
    Isolation isolation() default Isolation.DEFAULT;

    // 超时时间(秒),默认-1表示不超时
    int timeout() default -1;
    int timeoutString() default -1;

    // 只读事务,优化性能,适用于查询
    boolean readOnly() default false;

    // 哪些异常导致回滚(默认仅回滚 RuntimeException 和 Error)
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};

    // 哪些异常不回滚(即使它是 RuntimeException)
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}
2.1 传播行为(Propagation)
  • REQUIRED(默认):支持当前事务,如果不存在则新建。
  • SUPPORTS:支持当前事务,如果不存在则非事务执行。
  • MANDATORY:支持当前事务,如果不存在则抛出异常。
  • REQUIRES_NEW:新建事务,挂起当前事务(如果存在)。
  • NOT_SUPPORTED:非事务执行,如果存在事务则挂起。
  • NEVER:非事务执行,如果存在事务则抛出异常。
  • NESTED:如果当前存在事务,则在该事务内嵌套一个子事务(保存点);否则行为同 REQUIRED
2.2 隔离级别(Isolation)
  • DEFAULT:使用数据库默认隔离级别。
  • READ_UNCOMMITTED:读未提交(可能导致脏读、不可重复读、幻读)。
  • READ_COMMITTED:读已提交(避免脏读,Oracle默认)。
  • REPEATABLE_READ:可重复读(避免脏读和不可重复读,MySQL默认)。
  • SERIALIZABLE:串行化(最高隔离级别,性能低)。
2.3 只读事务
  • readOnly = true:当数据库支持时,会优化查询性能,也会禁止数据修改操作(如 INSERT/UPDATE)。
2.4 回滚规则
  • 默认只对 RuntimeException 及其子类Error 进行回滚,检查型异常(Exception 的子类且非 RuntimeException)不会自动回滚。
  • 可通过 rollbackFor 指定需要回滚的异常类型,noRollbackFor 指定不回滚的异常类型。

3. 代理对象错误详解:为什么事务会失效?

Spring 事务的生效依赖于代理对象。如果方法调用没有经过代理对象,那么 @Transactional 注解就不会被解析,事务逻辑不会执行。以下是常见的"代理对象错误"导致事务失效的场景。

3.1 自调用(内部方法调用)
java 复制代码
@Service
public class UserService {
    @Transactional
    public void addUser(User user) {
        // 数据库操作
        updateUser(user);          // 直接调用内部方法,绕过了代理!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
        // 数据库操作
    }
}

当外部调用 userService.addUser() 时,通过代理对象调用,addUser 的事务生效。但 addUser 内部直接通过 this.updateUser() 调用,相当于调用了目标对象的原始方法,没有经过代理,因此 updateUser 上的 @Transactional(REQUIRES_NEW) 被忽略,它和 addUser 使用同一个事务,或者根本不开启新事务。

3.2 方法非 public

Spring 默认只代理 public 方法(即使使用 CGLIB,默认也只拦截 public 方法)。如果 @Transactional 标注在 privateprotected 或默认访问权限的方法上,事务不会生效。

java 复制代码
@Transactional
private void update() { // 事务不生效
    // ...
}
3.3 类或方法被 final 修饰

CGLIB 通过生成子类来实现代理,如果目标类或方法被 final 修饰,则无法继承和重写,代理失效。JDK 动态代理基于接口,如果类没有实现接口且未被 final 修饰,则 Spring 使用 CGLIB。

3.4 异常被捕获后未抛出
java 复制代码
@Transactional
public void method() {
    try {
        // 可能抛出异常的操作
    } catch (Exception e) {
        // 仅打印日志,未抛出异常
        log.error("error", e);
        // 事务不会回滚,因为异常被吞没了
    }
}

事务管理器只能感知到从方法抛出的异常,如果异常被捕获且未重新抛出,则事务正常提交。

3.5 异常类型不匹配
java 复制代码
@Transactional   // 默认只回滚 RuntimeException
public void method() throws IOException {
    // 抛出检查型异常 IOException
    throw new IOException();
}

由于 IOException 不是 RuntimeException,事务不会回滚。需要指定 rollbackFor = IOException.class

3.6 多数据源时未指定正确的事务管理器
java 复制代码
@Transactional("primaryTxManager")
public void method() { ... }

如果未指定 valuetransactionManager,则使用默认的事务管理器。在多数据源场景下可能导致事务未绑定到正确的数据源。

3.7 数据库引擎不支持事务

如 MySQL 的 MyISAM 引擎不支持事务,即使代码正确也无法回滚。

3.8 代理方式不正确

如果 Bean 实现了接口,Spring 默认使用 JDK 动态代理。此时如果目标类中的方法未在接口中定义,则无法被代理(但可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB)。

3.9 在非 Spring 管理的对象中使用

如果对象不是由 Spring 容器管理(比如手动 new 出来的),那么即使标注了 @Transactional,Spring 也不会为其创建代理,事务自然无效。


4. 解决方案:确保事务通过代理对象执行

4.1 注入自身代理(推荐)
java 复制代码
@Service
public class UserService {
    @Autowired
    private UserService self;   // 注入自身代理

    @Transactional
    public void addUser(User user) {
        // ...
        self.updateUser(user);  // 通过代理调用,事务生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
        // ...
    }
}

注意:Spring 可以处理这种自注入,但需避免循环依赖,通常字段注入没问题。

4.2 使用 AopContext.currentProxy()
java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true) // 在配置类中开启
@Service
public class UserService {
    @Transactional
    public void addUser(User user) {
        ((UserService) AopContext.currentProxy()).updateUser(user);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
        // ...
    }
}

这种方式代码耦合度较高,且需要开启 exposeProxy

4.3 将事务方法拆分到不同的 Bean
java 复制代码
@Service
public class UserService {
    @Autowired
    private UserUpdateService updateService;

    @Transactional
    public void addUser(User user) {
        updateService.updateUser(user);
    }
}

@Service
public class UserUpdateService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
        // ...
    }
}

这样调用自然经过代理,清晰且易于测试。

4.4 使用编程式事务

如果不想依赖代理,可以直接使用 TransactionTemplatePlatformTransactionManager 手动控制事务。


5. 注意事项与最佳实践

  1. 首选 public 方法标注注解 ,避免放在 private 方法上。
  2. 避免内部方法自调用,通过注入自身代理或拆分 Bean 解决。
  3. 明确回滚规则 :如果需要回滚检查型异常,使用 rollbackFor 指定。
  4. 合理选择传播行为REQUIRES_NEWNESTED 会创建新事务或保存点,注意资源消耗。
  5. 只读事务优化 :对于纯查询方法,使用 readOnly = true 提升性能。
  6. 多数据源:确保每个事务指定正确的事务管理器。
  7. 测试事务是否生效 :在单元测试中故意抛出异常,检查数据是否回滚;或打印 this.getClass() 确认是否为代理类。
  8. 注意线程边界:事务与线程绑定,新开启的线程不会继承原有事务。
  9. 代理方式选择 :如果希望事务在类内非接口方法上生效,应强制使用 CGLIB(spring.aop.proxy-target-class=true@EnableAspectJAutoProxy(proxyTargetClass = true))。

6. 总结

@Transactional 是 Spring 提供的强大事务管理工具,但其底层依赖于 AOP 代理。要确保事务正确生效,必须保证调用链路上所有标注了 @Transactional 的方法都通过 Spring 代理对象执行。最常见的失效场景是 内部方法自调用非 public 方法 以及 异常被吞没 。通过注入自身代理、拆分 Bean 或使用 AopContext.currentProxy() 可以解决自调用问题。理解代理机制和事务属性,才能在复杂业务中灵活应用

事务失效原理剖析

Spring 声明式事务的核心是 AOP 代理:通过代理对象拦截目标方法,在方法执行前后添加事务管理逻辑(开启、提交、回滚)。事务管理器与当前线程绑定(TransactionSynchronizationManager),并通过异常类型决定提交或回滚。失效的根本原因在于 调用链未能经过代理对象事务管理器无法正确感知异常/数据源


1. 自调用(内部方法调用)

  • 原理 :外部调用时,调用的是代理对象的方法,代理对象会添加事务逻辑。但类内部通过 this 调用另一个方法时,this 指向原始对象而非代理对象,因此被调方法上的 @Transactional 不会触发任何拦截,事务逻辑不生效。

2. 非 public 方法

  • 原理 :Spring 事务拦截器 AbstractFallbackTransactionAttributeSource 默认只对 public 方法解析事务注解。即使 CGLIB 能代理非 public 方法,Spring 也出于安全设计默认不处理。代理对象无法在非 public 方法上织入事务增强。

3. 异常被吞没

  • 原理 :事务管理器通过方法执行抛出的异常决定回滚。若异常被 try-catch 捕获且未重新抛出,方法正常返回,事务管理器认为执行成功,执行 commit。回滚的触发依赖于异常沿调用栈传播到代理方法。

4. 异常类型不匹配

  • 原理@Transactional 默认回滚规则为 RuntimeExceptionError(基于 RuleBasedTransactionAttribute)。检查型异常(如 IOException)默认不回滚,因为 Spring 认为这类异常可能属于业务预期结果,除非通过 rollbackFor 显式指定。

5. 数据库引擎不支持事务

  • 原理 :Spring 只负责发出 begincommitrollback 命令,事务的原子性由底层数据库引擎保证。若引擎不支持事务(如 MyISAM),这些命令会被忽略或无效,数据修改即时生效,无法回滚。

6. final 类或方法

  • 原理 :Spring 默认使用 CGLIB 生成代理子类,通过继承目标类并重写方法实现拦截。若类或方法被 final 修饰,则无法被继承或重写,代理类无法插入事务拦截逻辑。

7. 非 Spring 管理的 Bean

  • 原理 :Spring 仅在容器管理的 Bean 上扫描 @Transactional 并生成代理。手动 new 的对象不属于 IoC 容器,没有代理过程,注解完全被忽略。

8. 多数据源未指定事务管理器

  • 原理 :当存在多个 PlatformTransactionManager 时,Spring 需要明确知道事务绑定到哪个数据源。若未通过 @Transactional("managerName") 指定,默认会选择第一个注册的事务管理器,可能导致事务操作错误的数据源,出现数据不一致。

9. 传播行为使用不当

  • 原理 :传播行为控制事务边界。例如 SUPPORTS:若当前无事务则不开启事务,方法以非事务方式运行;NOT_SUPPORTED:挂起当前事务,方法非事务执行。这些传播行为会导致方法没有事务上下文,注解形同虚设。

10. 代理方式限制(JDK 动态代理)

  • 原理:JDK 动态代理要求目标类实现接口,且代理对象仅能拦截接口中声明的方法。若事务方法仅在实现类中定义而未在接口中声明,则调用该方法时不会经过代理对象,事务失效。

核心结论 :事务生效的必要条件是 目标方法通过 Spring 代理对象调用,且事务管理器能正确识别异常和数据源。任何破坏这个条件的因素(调用路径绕过代理、注解位置不当、异常处理失当、底层基础设施缺失等)都会导致事务失效。。

相关推荐
jaysee-sjc2 小时前
【项目三】用GUI编程实现局域网群聊软件
java·开发语言·算法·安全·intellij-idea
无名-CODING2 小时前
Java 爬虫高级技术:反反爬策略与分布式爬虫实战
java·分布式·爬虫
jonyleek2 小时前
JVS物联网应用中控制器的四大职责和设备接入全流程
java·struts·servlet·私有化部署
csdn2015_2 小时前
java 将 List<Map<String,Object>> 类型里面的值转换为List<String>
java·windows·list
怀化纱厂球迷2 小时前
android车载应用动画-仿窗帘式下拉显示!Android 实现跟手裁剪动画 + RecyclerView 列表展示
android·java
牢七2 小时前
jfinal_cms-v5.1.0 白盒 nday
开发语言·python
ayt0072 小时前
Netty 4.2核心类解析:SingleThreadIoEventLoop的设计哲学与实现
java·网络
无名-CODING2 小时前
Java 爬虫进阶:动态网页、多线程与 WebMagic 框架实战
java·爬虫·okhttp
weixin_704266052 小时前
Spring 注解驱动开发与 Spring Boot 核心知识点梳理
java·spring boot·spring