Spring事务基础:你在入门时踩过的所有坑

Spring事务基础:你在入门时踩过的所有坑

开篇:用问题引起共鸣

  • 场景化提问:

    scss 复制代码
    @Service
    public class OrderService {
        @Transactional
        public void createOrder() {
            insertOrder();  // 插入订单
            updateStock();  // 更新库存(这里抛出异常)
        }
    }
  • 灵魂拷问: "为什么上面的代码事务不回滚?你遇到过多少种类似的情况?"

事务的概念:

Spring 事务管理是 Spring 框架的核心功能之一,用于在业务逻辑中保证数据的一致性和完整性。事务的本质是将一组操作(如数据库增删改查)封装为一个原子性操作,确保这些操作要么全部成功,要么全部失败。

事务的特性:

🧩 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部回滚。 🔗一致性(Consistency):事务执行前后,数据的状态保持一致。 🚧隔离性(Isolation):多个事务并发执行时,彼此之间互不干扰。 💾持久性(Durability):事务一旦提交,数据的修改就是永久性的。

ACID特性在Spring中的体现:

ACID属性 Spring实现方式 常见破坏场景
原子性 回滚机制(Rollback 异常被捕获未抛出
一致性 业务逻辑+约束 手动修改数据库绕过事务
隔离性 @Transactional(isolation=?) 脏读/幻读(隔离级别设置不当)
持久性 数据库提交 未提交事务时服务重启

🚩陷阱1:异常被吞掉

  • 错误示例

    java 复制代码
    @Transactional
    public void method() {
        try {
            jdbcTemplate.update("INSERT ...");
        } catch (Exception e) {
            log.error("出错", e); // 事务不回滚!
        }
    }
  • 问题分析

原理:在 Spring事务管理中,事务的回滚是基于异常触发的。默认情况下,只有 RuntimeException Error 会触发事务回滚,而受检异常(Checked Exception)不会触发回滚。因此,如果在代码中捕获了异常但没有重新抛出,Spring 就无法感知到异常的发生,从而不会触发事务回滚。

为什么事务不回滚? 异常被捕获并"吞掉" 在 catch 块中,异常被记录日志后没有重新抛出。Spring 的事务管理器通过代理机制监控方法执行过程中是否抛出了异常。如果没有异常抛出,事务管理器会认为操作成功,进而提交事务。 默认回滚规则限制 Spring 默认只对 RuntimeException Error 触发回滚。即使你手动抛出了一个受检异常(如 SQLException),事务也不会回滚。

  • 解决方案

    • 在catch中手动抛出throw new RuntimeException(e),捕获后抛出非受检(Unchecked)异常
    java 复制代码
    @Transactional
    public void method() {
        try {
            jdbcTemplate.update("INSERT ...");
        } catch (Exception e) {
            log.error("出错", e);
            throw new RuntimeException(e); // 手动抛出运行时异常
        }
    }
    • 或配置@Transactional(rollbackFor = Exception.class),显式声明对所有异常回滚。
    java 复制代码
    @Transactional(rollbackFor = Exception.class)
    public void method() {
        try {
            jdbcTemplate.update("INSERT ...");
        } catch (Exception e) {
            log.error("出错", e);
            throw e; // 重新抛出原始异常
        }
    }

🚩陷阱2:非public方法

  • 错误示例

    java 复制代码
    @Transactional
    private void innerMethod() { // 事务失效!
            // 数据库操作
    }
  • 问题分析

原理:在 Spring 框架中,事务管理是基于 AOP(面向切面编程)实现的。AOP 的核心机制是通过动态代理拦截目标方法的调用,并在方法执行前后添加事务管理逻辑。然而,Spring AOP 默认只能代理 public 方法,因此如果事务方法是非 public 的(如 private、protected 或包级私有),事务将无法生效。

为什么事务失效?

Spring AOP 的限制 Spring AOP 使用动态代理(JDK 动态代理或 CGLIB)来实现事务管理。动态代理只能拦截 public 方法的调用。对于非 public 方法(如 private、protected 或包级私有),代理对象无法拦截到这些方法的调用,因此事务管理逻辑不会生效。

  • 解决方案
    • 将方法改为 public 后,Spring AOP 能够通过代理对象拦截到该方法的调用,从而应用事务管理逻辑。
java 复制代码
@Transactional
public void innerMethod() {
          // 数据库操作
}

🚩陷阱3:自调用问题

  • 经典错误

    java 复制代码
    @Service
    public class UserService {
        public void updateUser() {
            this.innerMethod(); // 自调用导致AOP失效
        }
        
        @Transactional
        public void innerMethod() { /* ... */ }
    }
  • 问题分析

原理:在 Spring 中,事务管理是通过 AOP(面向切面编程)实现的。AOP 的核心机制是通过动态代理拦截目标方法的调用,并在方法执行前后添加事务管理逻辑。然而,当一个类中的方法通过this调用另一个方法时,这种调用会绕过代理对象,直接调用目标方法,从而导致 AOP 逻辑(如事务管理)失效。

  • 为什么事务失效?

    • Spring AOP的代理机制 Spring AOP 默认使用动态代理(JDK 动态代理或 CGLIB)来实现事务管理。动态代理会在目标对象外部创建一个代理对象,所有对目标方法的调用都会经过代理对象,从而触发事务管理逻辑。

    • 自调用绕过代理对象

      在上述代码中,updateUser() 方法通过 this.innerMethod() 直接调用了 innerMethod(),而不是通过代理对象调用。这种调用方式会绕过代理对象,导致事务管理逻辑无法生效。

  • 解决方案

    • 将事务方法移动到另一个类中,确保调用是通过代理对象进行的。
java 复制代码
@Service
public class UserService {
     @Autowired
     private InnerService innerService;
     public void updateUser() {
         innerService.innerMethod(); // 通过代理对象调用
     }
}
@Service
public class InnerService {
     @Transactional
     public void innerMethod() {
         // 数据库操作
     }
}
  • 通过AopContext.currentProxy()获取当前代理对象,并通过代理对象调用事务方法。
java 复制代码
@Service
public class UserService {

    public void updateUser() {
        ((UserService) AopContext.currentProxy()).innerMethod(); // 通过代理对象调用
    }
    
    @Transactional
    public void innerMethod() {
        // 数据库操作
    }
}

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)//启用暴露代理对象的功能
public class AppConfig {
}

🚩陷阱4:错误的事务传播机制

  • 经典错误

    java 复制代码
    @Transactional(propagation = Propagation.REQUIRED) // 默认
    public void methodA() {
        methodB(); // 不同传播行为的结果差异
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() { /* ... */ }
  • 问题分析

在 Spring 中,事务传播机制定义了事务方法之间的调用关系。不同的传播行为会导致事务的行为和结果产生显著差异。

  • 传播行为详解

    • REQUIRED(默认传播行为):如果当前存在事务,则加入该事务。如果当前不存在事务,则创建一个新事务。
    • REQUIRES_NEW:总是创建一个新事务。如果当前存在事务,则挂起当前事务。
  • 结论 :用流程图展示REQUIRED vs REQUIRES_NEW的区别

REQUIRED REQUIRES_NEW
调用 methodA ↓ 检查是否存在事务 ↓ 如果无事务 → 创建事务 A ↓ 调用 methodBmethodB 加入事务 A ↓ methodB 完成 ↓ methodA 完成 → 提交事务 A 调用 methodA ↓ 检查是否存在事务 ↓ 如果无事务 → 创建事务 A ↓ 调用 methodB ↓ 挂起事务 A ↓ 创建事务 B ↓ methodB 完成 → 提交事务 B ↓ 恢复事务 A ↓ methodA 完成 → 提交事务 A

可能的报错原因总结

  • 事务传播行为选择错误 如果需要独立事务但使用了 REQUIRED,可能会导致事务回滚影响范围过大。 如果需要共享事务但使用了 REQUIRES_NEW,可能会导致事务隔离性问题。
  • 事务嵌套复杂性 过多的事务嵌套可能导致性能问题或难以调试的事务行为。
  • 事务挂起和恢复开销 REQUIRES_NEW 会挂起当前事务并创建新事务,增加了系统开销。

🚩陷阱5:多数据源配置错误

  • 经典错误
java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    // 错误:事务管理器绑定了主数据源,但业务逻辑使用了从数据源
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

@Service
public class UserService {

    @Autowired
    private JdbcTemplate primaryJdbcTemplate;

    @Autowired
    private JdbcTemplate secondaryJdbcTemplate;

    @Transactional
    public void updateUser() {
        // 使用主数据源更新用户信息
        primaryJdbcTemplate.update("UPDATE user SET name = 'Alice' WHERE id = 1");

        // 使用从数据源记录日志
        secondaryJdbcTemplate.update("INSERT INTO log (message) VALUES ('User updated')");
    }
}
  • 问题分析

原理:在 Spring 应用中,如果项目需要操作多个数据源(如主库和从库、不同业务数据库等),必须正确配置事务管理器和数据源。如果事务管理器绑定到了错误的数据源,可能会导致以下问题: 数据一致性问题:事务无法正确管理目标数据源的操作。 SQL 执行失败:事务管理器尝试对未绑定的数据源执行事务操作。

  • 错误原因分析

    • 事务管理器绑定错误

      transactionManager 方法中绑定了主数据源(primaryDataSource),但 updateUser 方法中同时操作了主数据源和从数据源。 当 @Transactional 注解生效时,事务管理器只会管理主数据源的操作,而从数据源的操作不会被事务管理。

    • 数据一致性问题

      如果主数据源的更新成功,但从数据源的日志插入失败,事务管理器无法回滚从数据源的操作,导致数据不一致。

  • 修复方案

    • 需要为每个数据源配置独立的事务管理器,并通过注解指定事务管理器。
    java 复制代码
    @Configuration
    public class DataSourceConfig {
    
        @Bean(name = "primaryDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.primary")
        public DataSource primaryDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "secondaryDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.secondary")
        public DataSource secondaryDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        // 配置主数据源的事务管理器
        @Bean(name = "primaryTransactionManager")
        public PlatformTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    
        // 配置从数据源的事务管理器
        @Bean(name = "secondaryTransactionManager")
        public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
    @Service
    public class UserService {
    
        @Autowired
        @Qualifier("primaryJdbcTemplate")
        private JdbcTemplate primaryJdbcTemplate;
    
        @Autowired
        @Qualifier("secondaryJdbcTemplate")
        private JdbcTemplate secondaryJdbcTemplate;
    
        // 指定主数据源的事务管理器
        @Transactional("primaryTransactionManager")
        public void updateUser() {
            // 使用主数据源更新用户信息
            primaryJdbcTemplate.update("UPDATE user SET name = 'Alice' WHERE id = 1");
    
            // 手动调用从数据源操作(非事务)
            recordLog();
        }
    
        // 指定从数据源的事务管理器
        @Transactional("secondaryTransactionManager")
        public void recordLog() {
            secondaryJdbcTemplate.update("INSERT INTO log (message) VALUES ('User updated')");
        }
    }

🚩陷阱6:异步方法调用

  • 错误示范

    java 复制代码
    @Transactional
    public void mainMethod() {
        asyncTask(); // 异步方法内操作不回滚
    }
    
    @Async
    public void asyncTask() { 
     // 数据库操作
            jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");
    }
  • 问题分析

  • 原理:在 Spring 中,事务管理是基于线程绑定的(通过 ThreadLocal 实现)。当一个事务方法被异步调用时,事务上下文无法传递到异步线程中,从而导致事务管理失效。

  • 错误原因分析

    • 事务上下文未传递

      Spring 事务是基于线程绑定的,事务上下文存储在当前线程的 ThreadLocal 中。 当 asyncTask() 方法被异步调用时,它会在一个新的线程中执行,而新线程无法访问原线程的事务上下文。

    • 事务失效

      asyncTask() 方法中的数据库操作不会被事务管理器管理,因此即使发生异常,也不会触发回滚。

数据一致性问题

如果mainMethod()的事务提交成功,但 asyncTask() 的操作失败,会导致数据不一致。

  • 解决方案

    • 将事务逻辑封装到异步方法中,确保事务管理器能够正确管理异步线程中的操作,在asyncTask() 方法上添加@Transactional注解,并指定传播行为为 REQUIRES_NEW,确保每次调用都会创建一个新的事务。异步线程中的事务独立于主线程的事务,避免事务上下文丢失。
java 复制代码
  @Service
  public class UserService {

      @Transactional
      public void mainMethod() {
          asyncTask(); // 调用异步方法
      }
      
      @Async
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public void asyncTask() {
          // 数据库操作
          jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");
      }
  }
  • 如果异步任务需要跨多个服务或数据源操作,可以使用分布式事务框架(如 Seata、Atomikos)来保证数据一致性。分布式事务框架会协调多个服务或数据源的操作,确保所有操作在一个全局事务中完成。即使异步任务失败,也可以通过回滚机制保证数据一致性。
java 复制代码
//引入分布式事务框架依赖:
<dependency>
         <groupId>io.seata</groupId>
         <artifactId>seata-spring-boot-starter</artifactId>
         <version>1.5.0</version>
</dependency>

/**
配置分布式事务:
定义全局事务 ID。
使用框架提供的注解(如 @GlobalTransactional)管理事务。
*/
  @Service
  public class UserService {

      @GlobalTransactional
      public void mainMethod() {
          asyncTask(); // 异步方法内操作通过分布式事务管理
      }
      
      @Async
      public void asyncTask() {
          // 数据库操作
          jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");
      }
  }
  • 如果无法使用分布式事务,可以采用事件补偿机制,在异步任务失败时手动回滚或重试。通过记录操作日志和定义补偿逻辑,可以在异步任务失败时手动修复数据不一致问题。
java 复制代码
  @Service
  public class UserService {

      @Transactional
      public void mainMethod() {
          // 主方法逻辑
          jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");
      
          // 异步任务失败时触发补偿
          try {
              asyncTask();
          } catch (Exception e) {
              compensateTask();
          }
      }
      
      @Async
      public void asyncTask() {
          // 数据库操作
          jdbcTemplate.update("INSERT INTO log (message) VALUES ('User created')");
          if (Math.random() > 0.5) { // 模拟失败
              throw new RuntimeException("异步任务失败");
          }
      }
      
      public void compensateTask() {
          // 补偿逻辑
          jdbcTemplate.update("DELETE FROM user WHERE name = 'Alice'");
      }
  }

🚩陷阱7:长事务问题

  • 错误案例

@Service public class UserService {

csharp 复制代码
@Transactional
public void longRunningTask() {
    // 模拟长时间运行的操作
    for (int i = 0; i < 1000; i++) {
        jdbcTemplate.update("INSERT INTO user (name) VALUES ('User" + i + "')");
        try {
            Thread.sleep(100); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

}

  • 问题分析

  • 错误原因分析

    • 事务范围过大

      整个 longRunningTask 方法被包裹在一个事务中,事务持续时间长达 100 秒(1000 次循环 × 100 毫秒)。在此期间,数据库连接被长时间占用,可能导致连接池耗尽。

    • 资源锁定

      如果方法中涉及对表的写操作,可能会锁定大量行或表,导致其他事务等待或死锁。

    • 性能问题 数据库需要维护未提交事务的中间状态(如 Undo Log),增加了内存和磁盘的开销。

  • 危害:连接池耗尽、死锁风险

  • 解决方案

    • 将事务范围缩小到最小必要的范围,避免在事务中执行耗时操作。
java 复制代码
  @Service
  public class UserService {

      @Autowired
      private UserMapper userMapper;
      
      public void shortRunningTask() {
          // 将事务范围缩小到每次插入操作
          for (int i = 0; i < 1000; i++) {
              insertUser("User" + i);
              try {
                  Thread.sleep(100); // 模拟耗时操作
              } catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
              }
          }
      }
      
      @Transactional
      public void insertUser(String name) {
          userMapper.insert(name);
      }
  }
  • 检测工具

    sql 复制代码
    -- MySQL查看长事务
    SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(timediff(now(),trx_started)) > 60;

总结:事务使用Checklist

  • ✅ 检查方法是否为public
  • ✅ 检查异常是否未被捕获
  • ✅ 检查是否跨数据源/跨线程
  • ✅ 检查传播行为是否符合预期
  • ✅ 检查@Transactional注解是否被同类方法调用

附件:异常的分类

1、 受检异常(Checked Exception

  • 定义 受检异常是指在编译时就必须处理的异常。如果方法中可能抛出受检异常,则必须通过 try-catch 块捕获异常,或者通过 throws 关键字将异常向上抛出。
  • 特点 继承自java.lang.Exception类,但不包括 RuntimeException 及其子类。 编译器会强制要求开发者处理这些异常。 通常用于表示可恢复的业务逻辑错误或外部系统问题。
  • 常见示例 IOException:输入输出操作异常。 SQLException:数据库操作异常。 FileNotFoundException:文件未找到异常。

2、非受检异常(Unchecked Exception

  • 定义 非受检异常是指在编译时不需要显式处理的异常。即使方法中可能抛出非受检异常,编译器也不会强制要求开发者捕获或声明抛出。

  • 特点 继承自 java.lang.RuntimeExceptionjava.lang.Error。 不需要显式处理,但如果不处理,可能会导致程序崩溃。 通常用于表示程序逻辑错误或不可恢复的系统错误。

  • 常见示例

    运行时异常(RuntimeException):

    NullPointerException:空指针异常。 ArrayIndexOutOfBoundsException:数组越界异常。 IllegalArgumentException:非法参数异常。

    错误(Error):

    OutOfMemoryError:内存溢出错误。 StackOverflowError:栈溢出错误。

两者的区别

特性 受检异常(Checked Exception) 非受检异常(Unchecked Exception
继承关系 继承自 Exception,但不包括 RuntimeException 继承自 RuntimeExceptionError
编译时检查 编译器强制要求处理 编译器不要求显式处理
使用场景 可恢复的业务逻辑错误或外部系统问题 程序逻辑错误或不可恢复的系统错误
是否需要声明抛出
典型示例 IOException, SQLException NullPointerException, ArithmeticException
相关推荐
喵手11 分钟前
玩转Java网络编程:基于Socket的服务器和客户端开发!
java·服务器·网络
再见晴天*_*1 小时前
SpringBoot 中单独一个类中运行main方法报错:找不到或无法加载主类
java·开发语言·intellij idea
hdsoft_huge4 小时前
Java & Spring Boot常见异常全解析:原因、危害、处理与防范
java·开发语言·spring boot
雨白5 小时前
Java 多线程指南:从基础用法到线程安全
android·java
Hungry_Shark5 小时前
IDEA版本控制管理之使用Gitee
java·gitee·intellij-idea
赛姐在努力.5 小时前
《IDEA 突然“三无”?三秒找回消失的绿色启动键、主菜单和项目树!》
java·intellij-idea
猎板PCB黄浩5 小时前
从废料到碳减排:猎板 PCB 埋容埋阻的绿色制造革命,如何实现环保与性能双赢
java·服务器·制造
ZzzK,5 小时前
JAVA虚拟机(JVM)
java·linux·jvm
西红柿维生素5 小时前
JVM相关总结
java·jvm·算法
coderxiaohan6 小时前
【C++】类和对象1
java·开发语言·c++