当前位置: 首页 > news >正文

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

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

开篇:用问题引起共鸣
  • 场景化提问:

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

事务的概念:

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

事务的特性:

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

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

🚩陷阱1:异常被吞掉

  • 错误示例

    @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)异常
    @Transactional
    public void method() {try {jdbcTemplate.update("INSERT ...");} catch (Exception e) {log.error("出错", e);throw new RuntimeException(e); // 手动抛出运行时异常}
    }
    
    • 或配置@Transactional(rollbackFor = Exception.class),显式声明对所有异常回滚。
    @Transactional(rollbackFor = Exception.class)
    public void method() {try {jdbcTemplate.update("INSERT ...");} catch (Exception e) {log.error("出错", e);throw e; // 重新抛出原始异常}
    }
    

🚩陷阱2:非public方法

  • 错误示例

    @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 能够通过代理对象拦截到该方法的调用,从而应用事务管理逻辑。
@Transactional
public void innerMethod() {// 数据库操作
}

🚩陷阱3:自调用问题

  • 经典错误

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

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

  • 为什么事务失效?

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

    • 自调用绕过代理对象

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

  • 解决方案

    • 将事务方法移动到另一个类中,确保调用是通过代理对象进行的。
@Service
public class UserService {@Autowiredprivate InnerService innerService;public void updateUser() {innerService.innerMethod(); // 通过代理对象调用}
}
@Service
public class InnerService {@Transactionalpublic void innerMethod() {// 数据库操作}
}
  • 通过AopContext.currentProxy()获取当前代理对象,并通过代理对象调用事务方法。
@Service
public class UserService {public void updateUser() {((UserService) AopContext.currentProxy()).innerMethod(); // 通过代理对象调用}@Transactionalpublic void innerMethod() {// 数据库操作}
}@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)//启用暴露代理对象的功能
public class AppConfig {
}

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

  • 经典错误

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

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

  • 传播行为详解

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

REQUIREDREQUIRES_NEW
调用 methodA

检查是否存在事务

如果无事务 → 创建事务 A

调用 methodB

methodB 加入事务 A

methodB 完成

methodA 完成 → 提交事务 A
调用 methodA

检查是否存在事务

如果无事务 → 创建事务 A

调用 methodB

挂起事务 A

创建事务 B

methodB 完成 → 提交事务 B

恢复事务 A

methodA 完成 → 提交事务 A

可能的报错原因总结

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

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

  • 经典错误
@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();}// 错误:事务管理器绑定了主数据源,但业务逻辑使用了从数据源@Beanpublic PlatformTransactionManager transactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}@Service
public class UserService {@Autowiredprivate JdbcTemplate primaryJdbcTemplate;@Autowiredprivate JdbcTemplate secondaryJdbcTemplate;@Transactionalpublic 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 注解生效时,事务管理器只会管理主数据源的操作,而从数据源的操作不会被事务管理。

    • 数据一致性问题

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

  • 修复方案

    • 需要为每个数据源配置独立的事务管理器,并通过注解指定事务管理器。
    @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:异步方法调用

  • 错误示范

    @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,确保每次调用都会创建一个新的事务。异步线程中的事务独立于主线程的事务,避免事务上下文丢失。
  @Servicepublic class UserService {@Transactionalpublic void mainMethod() {asyncTask(); // 调用异步方法}@Async@Transactional(propagation = Propagation.REQUIRES_NEW)public void asyncTask() {// 数据库操作jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");}}
  • 如果异步任务需要跨多个服务或数据源操作,可以使用分布式事务框架(如 Seata、Atomikos)来保证数据一致性。分布式事务框架会协调多个服务或数据源的操作,确保所有操作在一个全局事务中完成。即使异步任务失败,也可以通过回滚机制保证数据一致性。
//引入分布式事务框架依赖:
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.5.0</version>
</dependency>/**
配置分布式事务:
定义全局事务 ID。
使用框架提供的注解(如 @GlobalTransactional)管理事务。
*/@Servicepublic class UserService {@GlobalTransactionalpublic void mainMethod() {asyncTask(); // 异步方法内操作通过分布式事务管理}@Asyncpublic void asyncTask() {// 数据库操作jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");}}
  • 如果无法使用分布式事务,可以采用事件补偿机制,在异步任务失败时手动回滚或重试。通过记录操作日志和定义补偿逻辑,可以在异步任务失败时手动修复数据不一致问题。
  @Servicepublic class UserService {@Transactionalpublic void mainMethod() {// 主方法逻辑jdbcTemplate.update("INSERT INTO user (name) VALUES ('Alice')");// 异步任务失败时触发补偿try {asyncTask();} catch (Exception e) {compensateTask();}}@Asyncpublic 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 {

@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),增加了内存和磁盘的开销。

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

  • 解决方案

    • 将事务范围缩小到最小必要的范围,避免在事务中执行耗时操作。
  @Servicepublic class UserService {@Autowiredprivate 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();}}}@Transactionalpublic void insertUser(String name) {userMapper.insert(name);}}
  • 检测工具

    -- 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, SQLExceptionNullPointerException, ArithmeticException
http://www.dtcms.com/a/339744.html

相关文章:

  • MoonBit Perals Vol.06: Moonbit 与 LLVM 共舞 (上):编译前端实现
  • 【深度解析】2025年中国GEO优化公司:如何驱动“答案营销”
  • python学习DAY46打卡
  • Vulkan笔记(十)-图形管道的七个配置项
  • 微服务-07.微服务拆分-微服务项目结构说明
  • VulKan笔记(九)-着色器
  • Qt消息队列
  • MySQL深分页性能优化实战:大数据量情况下如何进行优化
  • MySQL 三大日志:redo log、undo log、binlog 详解
  • 面试题储备-MQ篇 1-说说你对RabbitMQ的理解
  • 3D检测笔记:MMDetection3d环境配置
  • 基于单片机智能手环/健康手环/老人健康监测
  • DataSourceAutoConfiguration源码笔记
  • 47 C++ STL模板库16-容器8-关联容器-集合(set)多重集合(multiset)
  • Lec. 2: Pytorch, Resource Accounting 课程笔记
  • 告别手写文档!Spring Boot API 文档终极解决方案:SpringDoc OpenAPI
  • 一文速通Ruby语法
  • GeoTools 读取影像元数据
  • 常见 GC 收集器与适用场景:从吞吐量到亚毫秒停顿的全景指南
  • Kotlin 相关知识点
  • 驱动开发系列66 - glCompileShader实现 - GLSL中添加内置函数
  • 从“为什么”到“怎么做”——Linux Namespace 隔离实战全景地图
  • [激光原理与应用-309]:光学设计 - 什么是光学系统装配图,其用途、主要内容、格式与示例?
  • 线性基 系列
  • Java static关键字
  • OptiTrack光学跟踪系统,提高机器人活动精度
  • 讯飞星火语音大模型
  • CAD图纸如何批量转换成PDF格式?
  • 机器学习概念(面试题库)
  • 部署tomcat应用时注意事项