Spring @Transactional 教程

第一章:事务管理的演进之路

要理解 @Transactional 的优雅,我们先要看看它解决了什么“痛苦”。

1.1 编程式事务

在 Spring 早期,开发者需要手动控制事务的每一个环节,这被称为编程式事务。它主要依赖 PlatformTransactionManager 和 TransactionTemplate 等API。

方式一:使用 PlatformTransactionManager

这是一种非常底层的实现,代码与业务逻辑高度耦合,事务的生命周期从getTransaction()开始,到commit()rollback()结束。充满了模板代码:

      @Test
public void transactionManagerTest() {
    // 1. 定义事务属性(隔离级别、传播行为等)
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    // 2. 获取事务状态
    TransactionStatus status = transactionManager.getTransaction(def);

    try {
        log.info("开始执行业务操作...");
        // 核心业务逻辑
        User user = new User().setName("张三");
        userService.insertUser(user);

        // 模拟异常
        int i = 1 / 0;

        // 3. 如果一切正常,提交事务
        transactionManager.commit(status);
        log.info("事务成功提交");
    } catch (Exception e) {
        // 4. 如果出现异常,回滚事务
        transactionManager.rollback(status);
        log.error("业务异常,事务已回滚", e);
    }
}
    

痛点分析:

  • 代码冗余:每个需要事务的方法都得写一套 try-catch-finally 的模板。

  • 高度耦合:事务管理代码(开启、提交、回滚)和核心业务代码(insertUser)混杂在一起,难以维护。

方式二:使用 TransactionTemplate (稍作改进)

Spring 提供了 TransactionTemplate 来简化上述过程,它利用了回调模式,但本质上仍是编程式的。

      @Test
public void transactionTemplateTest() {
    transactionTemplate.execute(status -> {
        try {
            log.info("开始执行业务操作...");
            User user = new User().setName("张三");
            userService.insertUser(user);

            // 模拟异常
            int i = 1 / 0;
            return "成功";
        } catch (Exception e) {
            log.error("业务异常,手动标记事务为回滚", e);
            // 出现异常时,必须手动标记事务状态为“仅回滚”
            status.setRollbackOnly();
            return "失败";
        }
    });
}
    

虽然代码稍微简洁了一些,但业务逻辑依然被包裹在事务模板中,耦合问题没有根本解决。

1.2 优雅的方案:声明式事务 (@Transactional)

为了将开发者从繁琐的事务管理中解放出来,Spring 引入了基于 AOP 的声明式事务。你只需要一个注解,就能优雅地完成所有工作。

Generated java

      @Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    // 只需一个注解,Spring 会在方法前后自动处理事务
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createUserAndThrowException(User user) {
        log.info("开始执行业务操作...");
        // 核心业务逻辑
        userDao.insert(user);
        
        // 模拟异常
        int i = 1 / 0;
    }
}
    

优势对比:

  • 代码极简:没有一行多余的事务管理代码,专注于业务本身。

  • 关注点分离:事务管理(横切关注点)与业务逻辑(核心关注点)完全解耦,符合 AOP 思想,代码更清晰、更易于维护。

现在,你已经直观地感受到了 @Transactional 的强大。接下来,我们深入其内部,看看魔法是如何发生的。


第二章:深入 @Transactional 核心

2.1 工作原理:AOP 与动态代理

@Transactional 的实现原理是 Spring AOP动态代理

  1. 代理创建:当 Spring 容器启动时,它会扫描到带有 @Transactional 注解的 Bean。Spring 不会直接返回这个 Bean 的实例,而是为它创建一个代理对象

  2. 方法拦截:当外部代码调用这个 Bean 的 public 方法时,实际上是调用了代理对象的方法。

  3. 事务增强:代理对象就像一个“保安”,它会在调用真实方法前执行“前置操作”(开启事务),在调用真实方法后执行“后置操作”(提交或回滚事务)。

执行流程图:

理解这个代理机制是掌握所有“失效场景”的关键!

2.2 注解的适用范围
  • 类 (Class):当注解在类上时,表示该类所有 public 方法都应用相同的事务配置。

  • 方法 (Method):当方法和类上都有注解时,方法级别的注解会覆盖类级别的配置。这是更精细的控制方式。

  • 接口 (Interface)不推荐使用。虽然技术上可行(如果使用 JDK 动态代理),但这是一种糟糕的实践。因为它会将事务这种实现细节耦合到接口定义中。如果切换到 CGLIB 代理(基于类的代理),接口上的注解会直接失效。

    最佳实践:始终将 @Transactional 注解在具体的实现类或其方法上。

2.3 核心属性精讲

@Transactional 提供了丰富的属性来精细控制事务行为。

属性说明常用值/重要说明
propagation事务传播行为 (最重要)。定义当一个事务方法调用另一个事务方法时,事务如何交互。REQUIRED (默认), REQUIRES_NEW, NESTED
isolation事务隔离级别。定义事务并发问题的处理能力(脏读、不可重复读、幻读)。READ_COMMITTED (常用), REPEATABLE_READ (MySQL默认)
readOnly只读事务。设置为 true 可进行性能优化,数据库会跳过一些锁定和日志记录。true, false (默认)
timeout事务超时 (秒)。超过指定时间未完成,事务将自动回滚。整数值,如 30
rollbackFor指定回滚的异常。定义哪些异常类型会触发回滚。Exception.class (强烈推荐)
noRollbackFor指定不回滚的异常。定义哪些异常类型不会触发回滚。业务自定义的非关键异常

propagation (传播行为) 深度解析:

  • REQUIRED (默认):如果当前有事务,就加入;如果没有,就新建一个。这是最常见的场景。

  • REQUIRES_NEW:总是创建一个全新的、独立的事务。如果外部已有事务,会将其挂起。常用于需要独立提交的日志记录等场景。即使内部事务回滚,也不会影响外部事务。

  • NESTED:(注意:与REQUIRED不同!) 如果当前有事务,则创建一个嵌套事务(保存点 Savepoint)。嵌套事务是外部事务的一部分,它回滚不会影响外部事务。但如果外部事务回滚,嵌套事务也必须回滚。这需要数据库驱动支持保存点特性。

rollbackFor (回滚规则) 的关键点:

注意! Spring 默认只在捕获到 RuntimeException (未检查异常)Error 时才会回滚事务。对于 Exception 的子类 (受检异常,如 IOException),默认是不会回滚的!

这是一个巨大的“坑”。为了避免意外,养成良好习惯:
@Transactional(rollbackFor = Exception.class)
这样可以确保任何异常都会触发回滚。


第三章:@Transactional 失效的6大场景与对策

由于其代理原理,@Transactional 在某些情况下会“失效”。

1. 失效场景一:应用在非 public 方法上
  • 场景:@Transactional 写在了 private, protected 或 default 权限的方法上。

  • 原因:Spring AOP 的动态代理(无论是 JDK 还是 CGLIB)主要是为 public 方法设计的。在事务拦截器的源码中,会检查方法是否为 public,如果不是,则直接跳过,不会应用事务增强。

  • 解决方案始终将 @Transactional 应用于 public 方法。

2. 失效场景二:方法内部调用 (this 调用)

这是最常见也最隐蔽的失效场景。

  • 场景

          @Service
    public class MyService {
        public void methodA() {
            // this 调用,绕过了代理对象,导致 methodB 的事务失效!
            this.methodB(); 
        }
    
        @Transactional
        public void methodB() {
            // ... 数据库操作 ...
        }
    }
        

  • 原因:this 关键字引用的是原始对象,而不是 Spring 创建的代理对象。调用 this.methodB() 直接执行了原始对象的代码,完全绕过了代理的事务拦截逻辑。

  • 解决方案 (按推荐度排序)

    1. 最佳方案:拆分到另一个 Service:将 methodB 移到另一个 Service (如 ServiceB) 中,然后在 MyService 中 @Autowired ServiceB,通过 serviceB.methodB() 调用。这符合单一职责原则,是架构上最清晰的做法。

    2. 推荐方案:注入自身代理:通过 AopContext.currentProxy() 获取当前方法的代理对象来调用。

            // 启动类或配置类上需要加 @EnableAspectJAutoProxy(exposeProxy = true)
      @Service
      public class MyService {
          public void methodA() {
              // 获取代理对象,通过代理对象调用
              ((MyService) AopContext.currentProxy()).methodB();
          }
      
          @Transactional
          public void methodB() { /* ... */ }
      }
          
    3. 可行但不推荐:注入自身:@Autowired 注入自己。

       
            @Service
      public class MyService {
          @Autowired
          private MyService self; // 注入自身的代理对象
      
          public void methodA() {
              self.methodB(); // 通过代理调用
          }
      
          @Transactional
          public void methodB() { /* ... */ }
      }
          

      注意:这可能导致循环依赖问题,尽管 Spring 能解决大部分场景。

3. 失效场景三:异常被 try-catch 吞没
  • 场景

    Generated java
          @Transactional(rollbackFor = Exception.class)
    public void transactionalTest() {
        try {
            // ... 数据库操作 ...
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("执行事务异常,但异常被捕获了", e);
            // 异常被“吃掉”,没有重新抛出,代理对象认为方法正常结束,会提交事务!
        }
    }
        

  • 原因:代理对象只能通过捕获从方法中抛出的异常来决定是否回滚。如果异常在方法内部被 catch 块完全处理(吞没),代理就无法感知到任何问题,会按正常流程提交事务。

  • 解决方案:在 catch 块中处理完必要逻辑(如记录日志)后,必须将异常重新抛出

     
          catch (Exception e) {
        log.error("执行事务异常,需要回滚", e);
        // 将异常重新抛出,让事务代理可以感知到
        throw e; 
        // 或者抛出自定义的运行时异常
        // throw new MyBusinessException("操作失败", e);
    }
        

4. 失效场景四:rollbackFor 属性设置错误
  • 场景:方法中抛出了一个受检异常(如 IOException),但 @Transactional 没有指定 rollbackFor。

  • 原因:如前所述,Spring 默认只对 RuntimeException 和 Error 回滚。

  • 解决方案:明确指定 rollbackFor = Exception.class 或你期望回滚的具体异常类型。

5. 失效场景五:propagation 属性设置错误
  • 场景:希望开启事务,却错误地配置了 propagation。

  • 原因:如果传播行为被设置为 NOT_SUPPORTED (以非事务方式运行,若有事务则挂起)、NEVER (以非事务方式运行,若有事务则抛异常) 或 SUPPORTS (有事务就加入,没事务就算了),在外部没有事务的情况下,这些方法本身是不会主动开启事务的。

  • 解决方案:确保需要事务的方法使用了正确的传播行为,通常是默认的 REQUIRED。

6. 失效场景六:数据库引擎不支持事务
  • 场景:代码完全正确,但事务就是不回滚。

  • 原因:事务的最终执行者是数据库。如果你的 MySQL 表使用了 MyISAM 存储引擎,它天生就不支持事务。

  • 解决方案:检查你的表结构,确保使用了支持事务的存储引擎,如 InnoDB


第四章:总结

@Transactional 是一个强大的工具,使用时请遵循以下最佳实践:

  1. 优先使用声明式事务:它让你的代码更简洁、更专注。

  2. 注解于具体实现类:将 @Transactional 放在 Service 实现类或其 public 方法上,而非接口。

  3. 明确回滚规则:为避免意外,建议总是设置 rollbackFor = Exception.class。

  4. 善用只读事务:对于所有查询操作,添加 @Transactional(readOnly = true) 可以有效提升性能。

  5. 警惕内部调用:深刻理解代理原理,避免 this 调用导致事务失效,优先选择重构代码。

  6. 保持事务粒度小:事务方法应只包含必要的数据库操作,避免将耗时的、非事务性的操作(如RPC、文件IO)放入其中,以免长时间占用数据库连接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值