作为 Java 后端开发,Spring 事务是我们每天都在使用的核心功能。它通过简单的@Transactional注解,就能让我们轻松实现事务管理,保证数据的一致性。但如果使用不当,@Transactional注解可能会悄无声息地失效,导致数据不一致、脏数据等严重的线上问题。
面试时,Spring 事务更是 100% 的必考题,面试官会从原理到实践层层深挖:
- Spring 事务的底层原理是什么?
@Transactional注解为什么会失效?- 事务的 7 种传播行为分别是什么?
- 什么是事务的隔离级别?解决了什么问题?
- 同类中方法调用为什么会导致事务失效?
这篇文章,我们就从核心原理、注解配置、失效场景、传播行为、隔离级别五个维度,全面拆解 Spring 事务。不仅会讲清楚理论,更会提供可直接落地的代码示例和最佳实践,让你看完既能轻松应对面试,又能解决实际项目中的事务问题。

一、先搞懂:什么是事务?
事务是一组原子性的 SQL 操作,这组操作要么全部执行成功,要么全部执行失败,不会出现部分成功部分失败的情况。
最经典的例子就是银行转账:A 向 B 转账 100 元,这个操作包含两个步骤:
- A 的账户余额减少 100 元
- B 的账户余额增加 100 元
如果没有事务,可能会出现 A 的钱扣了,但 B 的钱没加的情况,导致数据不一致。而事务可以保证这两个步骤要么都成功,要么都失败,不会出现中间状态。
事务的 ACID 四大特性
所有的事务都必须满足 ACID 四大特性:
- 原子性(Atomicity):事务是一个不可分割的最小单位,所有操作要么全部执行,要么全部不执行
- 一致性(Consistency):事务执行前后,数据的完整性约束没有被破坏
- 隔离性(Isolation):多个事务并发执行时,每个事务之间相互隔离,互不影响
- 持久性(Durability):事务提交后,对数据的修改是永久的,即使系统崩溃也不会丢失
事务的浅显易懂讲解具体在下面文章的前几段 MySQL 事务全解:从 ACID 特性到并发问题,再到底层实现与线上最佳实践
https://blog.csdn.net/2401_88151415/article/details/160910978
二、Spring 事务基础
Spring 事务是对 JDBC 事务的封装,它提供了两种事务管理方式:编程式事务 和声明式事务。
1. 编程式事务
编程式事务是通过编写代码来管理事务,需要手动开启事务、提交事务和回滚事务。
java
@Autowired
private PlatformTransactionManager transactionManager;
public void transfer(String from, String to, int amount) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 扣减A的余额
accountMapper.decreaseBalance(from, amount);
// 增加B的余额
accountMapper.increaseBalance(to, amount);
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
优点:灵活,可以精确控制事务的边界
缺点:代码侵入性强,重复代码多,容易出错
2. 声明式事务(推荐)
声明式事务是通过注解或 XML 配置来管理事务,不需要编写任何事务管理的代码。Spring 通过 AOP 动态代理,在方法执行前后自动开启、提交或回滚事务。
java
@Transactional
public void transfer(String from, String to, int amount) {
// 扣减A的余额
accountMapper.decreaseBalance(from, amount);
// 增加B的余额
accountMapper.increaseBalance(to, amount);
}
优点:代码侵入性低,使用简单,便于维护
缺点:灵活性不如编程式事务
核心结论:99% 的业务场景都应该使用声明式事务,只有在需要精确控制事务边界的特殊场景下,才使用编程式事务。
3. 声明式事务的核心原理:AOP 动态代理
声明式事务的底层原理是AOP 动态代理 。当我们给一个方法加上@Transactional注解时,Spring 会为这个类生成一个代理对象,在代理对象的方法执行前后,自动添加事务管理的逻辑。
完整的执行流程:
- 客户端调用代理对象的方法
- 代理对象在方法执行前,通过事务管理器开启事务
- 代理对象执行目标方法
- 如果目标方法执行成功,代理对象提交事务
- 如果目标方法抛出异常,代理对象回滚事务
Spring 支持两种动态代理方式:
- JDK 动态代理:基于接口实现,只能代理实现了接口的类
- CGLIB 动态代理:基于继承实现,可以代理没有实现接口的类
Spring Boot 2.0 以后,默认使用 CGLIB 动态代理。
三、@Transactional注解核心参数详解
@Transactional注解有很多参数,合理配置这些参数可以让事务更符合业务需求。下面是最常用的几个参数:(平时开发最常用的就是指定rollbackFor,其余了解即可)
| 参数 | 含义 | 默认值 | 说明 |
|---|---|---|---|
value/transactionManager |
指定事务管理器 | 空 | 当有多个事务管理器时,需要指定使用哪个 |
propagation |
事务传播行为 | Propagation.REQUIRED |
定义事务之间的嵌套关系 |
isolation |
事务隔离级别 | Isolation.DEFAULT |
定义多个并发事务之间的隔离程度 |
timeout |
事务超时时间 | -1(永不超时) | 事务执行超过指定时间后自动回滚 |
readOnly |
是否只读事务 | false | 标记事务为只读,优化查询性能 |
rollbackFor |
需要回滚的异常类型 | RuntimeException和Error |
指定哪些异常会导致事务回滚 |
noRollbackFor |
不需要回滚的异常类型 | 空 | 指定哪些异常不会导致事务回滚 |
四、12 种常见的事务失效场景(面试必问)
这是本文的核心部分,也是实际项目中最容易踩坑的地方。我整理了 12 种最常见的事务失效场景,每个场景都包含失效原因、错误代码示例、正确代码示例和解决方案。
场景 1:方法不是 public 的
失效原因 :Spring 的 AOP 动态代理只能拦截 public 方法,非 public 方法不会被代理,因此@Transactional注解不会生效。
错误示例:
java
// 错误:private方法,事务不会生效
@Transactional
private void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
正确示例:
java
// 正确:public方法
@Transactional
public void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
解决方案:所有需要事务的方法都要声明为 public。
场景 2:同类中方法自调用
失效原因:当一个类中的方法调用本类的另一个方法时,调用的是原对象的方法,而不是代理对象的方法,因此不会被 AOP 拦截,事务不会生效。
这是最常见也是最容易被忽略的事务失效场景。
错误示例:
java
@Service
public class AccountService {
public void transfer(String from, String to, int amount) {
// 调用本类的方法,事务不会生效
doTransfer(from, to, amount);
}
@Transactional
public void doTransfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
}
解决方案:
1.注入自己的代理对象:
java
@Service
public class AccountService {
@Autowired
private AccountService accountService; // 注入自己的代理对象
public void transfer(String from, String to, int amount) {
// 调用代理对象的方法,事务会生效
accountService.doTransfer(from, to, amount);
}
@Transactional
public void doTransfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
}
2.使用AopContext.currentProxy()获取代理对象:
java
@Service
public class AccountService {
public void transfer(String from, String to, int amount) {
// 获取当前代理对象
AccountService proxy = (AccountService) AopContext.currentProxy();
// 调用代理对象的方法,事务会生效
proxy.doTransfer(from, to, amount);
}
@Transactional
public void doTransfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
}
3.将事务方法放到另一个类中:
java
@Service
public class AccountService {
@Autowired
private TransactionService transactionService;
public void transfer(String from, String to, int amount) {
transactionService.doTransfer(from, to, amount);
}
}
@Service
public class TransactionService {
@Transactional
public void doTransfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
}
场景 3:异常类型不是 RuntimeException 或 Error
失效原因 :Spring 事务默认只会在抛出RuntimeException(运行时异常)和Error(错误)时回滚,对于受检异常(Checked Exception)不会回滚。
受检异常 :
Exception及其子类(非 RuntimeException),编译强制处理 ,Spring 事务默认不回滚 如
Exception(顶级受检异常)IOException(文件读写失败)SQLException(数据库操作错误)ParseException(字符串解析失败)非受检异常 :
RuntimeException+Error,运行时抛出 ,Spring 事务默认自动回滚 如
- RuntimeException (运行时异常)
- 空指针
NullPointerException- 数组越界
IndexOutOfBoundsException- 算数除零
ArithmeticException- Error (系统错误,程序搞不定)
- 内存溢出
OutOfMemoryError
错误示例:
java
@Transactional
public void transfer(String from, String to, int amount) throws Exception {
accountMapper.decreaseBalance(from, amount);
// 抛出受检异常,事务不会回滚
throw new Exception("转账失败");
}
解决方案 :在@Transactional注解中指定rollbackFor = Exception.class,让所有异常都触发回滚。
正确示例:
java
// 正确:指定所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) throws Exception {
accountMapper.decreaseBalance(from, amount);
throw new Exception("转账失败");
}
场景 4:手动捕获异常没有抛出
失效原因:如果在方法中手动捕获了异常,并且没有重新抛出,那么 Spring 不会感知到异常,也就不会回滚事务。
错误示例:
java
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
try {
accountMapper.decreaseBalance(from, amount);
int i = 1 / 0; // 抛出ArithmeticException
accountMapper.increaseBalance(to, amount);
} catch (Exception e) {
// 手动捕获异常,没有抛出,事务不会回滚
e.printStackTrace();
}
}
解决方案:捕获异常后重新抛出,或者手动设置事务回滚。
正确示例 1:重新抛出异常:
java
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
try {
accountMapper.decreaseBalance(from, amount);
int i = 1 / 0;
accountMapper.increaseBalance(to, amount);
} catch (Exception e) {
e.printStackTrace();
// 重新抛出异常,事务会回滚
throw e;
}
}
正确示例 2:手动设置事务回滚:
java
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
try {
accountMapper.decreaseBalance(from, amount);
int i = 1 / 0;
accountMapper.increaseBalance(to, amount);
} catch (Exception e) {
e.printStackTrace();
// 手动设置事务回滚
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
status.setRollbackOnly();
}
}
场景 5:错误的事务传播行为
失效原因:如果配置了错误的事务传播行为,可能会导致事务不会按照预期的方式执行。
最常见的错误是使用了Propagation.NOT_SUPPORTED或Propagation.NEVER,这两种传播行为会以非事务方式运行。
错误示例:
java
// 错误:使用NOT_SUPPORTED传播行为,会以非事务方式运行
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
解决方案 :根据业务需求选择正确的传播行为,绝大多数场景下使用默认的Propagation.REQUIRED即可。
场景 6:多线程环境
失效原因:Spring 事务是基于线程的,每个线程有自己的事务上下文。如果在方法中开启了新的线程,那么新线程中的操作不会被包含在原事务中。
错误示例:
java
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
// 扣减A的余额,在主线程中执行,会被事务管理
accountMapper.decreaseBalance(from, amount);
// 开启新线程,增加B的余额,不会被事务管理
new Thread(() -> {
accountMapper.increaseBalance(to, amount);
}).start();
// 主线程抛出异常,只会回滚主线程的操作,新线程的操作不会回滚
throw new RuntimeException("转账失败");
}
解决方案 :不要在事务方法中开启新线程,如果需要异步操作,使用 Spring 的@Async注解,并配置异步事务。
场景 7:数据库不支持事务
失效原因:如果数据库本身不支持事务,那么无论怎么配置 Spring 事务,都不会生效。
最常见的情况是使用 MySQL 的 MyISAM 引擎,MyISAM 引擎不支持事务,只有 InnoDB 引擎支持事务。
解决方案:将数据库表的引擎改为 InnoDB。
-- 修改表引擎为InnoDB
ALTER TABLE account ENGINE = InnoDB;
场景 8:方法被 final 或 static 修饰
失效原因:
- final 方法不能被重写,因此无法生成代理对象
- static 方法属于类,不属于对象,无法被代理
错误示例:
java
// 错误:final方法,无法被代理,事务不会生效
@Transactional
public final void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
// 错误:static方法,无法被代理,事务不会生效
@Transactional
public static void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
解决方案:不要给事务方法添加 final 或 static 修饰符。
场景 9:类没有被 Spring 管理
失效原因 :如果类没有被 Spring 管理(没有加@Service、@Component等注解),那么 Spring 无法为其生成代理对象,事务不会生效。
错误示例:
java
// 错误:没有加@Service注解,没有被Spring管理
public class AccountService {
@Transactional
public void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
}
解决方案 :给类添加@Service或@Component注解,让 Spring 管理这个类。
场景 10:错误的 rollbackFor 属性
失效原因 :如果rollbackFor属性指定的异常类型不对,可能会导致事务不会回滚。
最常见的错误是指定了异常的父类,但抛出的是子类异常,或者反过来。
错误示例:
java
// 错误:只指定了NullPointerException回滚,但抛出的是ArithmeticException,事务不会回滚
@Transactional(rollbackFor = NullPointerException.class)
public void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
int i = 1 / 0; // 抛出ArithmeticException
accountMapper.increaseBalance(to, amount);
}
解决方案 :除非有特殊需求,否则统一使用rollbackFor = Exception.class,让所有异常都触发回滚。
场景 11:嵌套事务的坑
失效原因:如果对嵌套事务的传播行为理解不到位,可能会导致事务不会按照预期的方式回滚。
最常见的错误是使用Propagation.REQUIRES_NEW时,内部事务回滚导致外部事务也回滚,或者反过来。
错误示例:
java
@Service
public class AccountService {
@Autowired
private LogService logService;
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
// 记录日志,即使日志记录失败,转账也应该成功
logService.addLog("转账成功");
}
}
@Service
public class LogService {
// 错误:使用默认的REQUIRED传播行为,日志记录失败会导致整个转账事务回滚
@Transactional(rollbackFor = Exception.class)
public void addLog(String message) {
logMapper.insert(message);
throw new RuntimeException("日志记录失败");
}
}
解决方案 :对于不需要影响外部事务的内部操作,使用Propagation.REQUIRES_NEW传播行为,让内部事务独立运行。
正确示例:
java
@Service
public class LogService {
// 正确:使用REQUIRES_NEW传播行为,内部事务独立运行
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addLog(String message) {
logMapper.insert(message);
throw new RuntimeException("日志记录失败");
}
}
场景 12:事务超时时间设置不合理
失效原因:如果事务执行时间超过了设置的超时时间,事务会自动回滚。如果超时时间设置太短,可能会导致正常的业务操作被回滚。
错误示例:
java
// 错误:超时时间设置为1秒,业务操作需要2秒,会导致事务自动回滚
@Transactional(timeout = 1, rollbackFor = Exception.class)
public void transfer(String from, String to, int amount) {
// 模拟耗时2秒的业务操作
Thread.sleep(2000);
accountMapper.decreaseBalance(from, amount);
accountMapper.increaseBalance(to, amount);
}
解决方案:根据业务的实际执行时间,合理设置超时时间。如果不确定,可以使用默认值(永不超时)。
五、事务的 7 种传播行为详解
事务传播行为定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring 提供了 7 种事务传播行为:
表格
| 传播行为 | 含义 | 说明 |
|---|---|---|
REQUIRED(默认) |
如果当前没有事务,就创建一个新事务;如果已经有事务,就加入到这个事务中 | 最常用的传播行为,适合绝大多数场景 |
REQUIRES_NEW |
无论当前有没有事务,都创建一个新的事务 | 适合内部操作需要独立事务的场景,比如日志记录 |
NESTED |
如果当前有事务,就嵌套在这个事务中运行;如果没有事务,就创建一个新事务 | 嵌套事务,内部事务回滚不会影响外部事务,外部事务回滚会影响内部事务 |
SUPPORTS |
如果当前有事务,就加入到这个事务中;如果没有事务,就以非事务方式运行 | 适合查询操作 |
NOT_SUPPORTED |
以非事务方式运行,如果当前有事务,就将当前事务挂起 | 适合不需要事务的操作 |
NEVER |
以非事务方式运行,如果当前有事务,就抛出异常 | 严格要求非事务的场景 |
MANDATORY |
如果当前有事务,就加入到这个事务中;如果没有事务,就抛出异常 | 严格要求在事务中运行的场景 |
重点讲解三种最常用的传播行为:
REQUIRED:默认值,也是最常用的。如果外部方法有事务,内部方法就加入外部事务;如果外部方法没有事务,内部方法就创建一个新事务。REQUIRES_NEW:总是创建一个新的事务,内部事务和外部事务是独立的。内部事务回滚不会影响外部事务,外部事务回滚也不会影响内部事务。NESTED:嵌套事务,内部事务是外部事务的一个子事务。内部事务回滚只会回滚自己的操作,不会影响外部事务;但外部事务回滚会导致内部事务也回滚。
六、事务的 4 种隔离级别详解
事务隔离级别定义了多个并发事务之间的隔离程度。隔离级别越高,数据一致性越好,但并发性能越差。
并发事务可能导致的三个问题
- 脏读:一个事务读取了另一个事务未提交的数据
- 不可重复读:一个事务内两次读取同一数据,结果不一致(因为另一个事务修改了数据并提交)
- 幻读:一个事务内两次查询同一范围的数据,结果不一致(因为另一个事务插入了新数据)
4 种隔离级别
表格
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
READ_UNCOMMITTED(读未提交) |
✅ | ✅ | ✅ | 最低的隔离级别,允许读取未提交的数据 |
READ_COMMITTED(读已提交) |
❌ | ✅ | ✅ | 只能读取已经提交的数据,解决了脏读问题 |
REPEATABLE_READ(可重复读) |
❌ | ❌ | ✅ | 同一个事务内多次读取同一数据结果一致,解决了脏读和不可重复读问题 |
SERIALIZABLE(串行化) |
❌ | ❌ | ❌ | 最高的隔离级别,所有事务串行执行,解决了所有问题,但性能最差 |
MySQL 的默认隔离级别是REPEATABLE_READ,Spring 的默认隔离级别是Isolation.DEFAULT,表示使用数据库的默认隔离级别。
七、最佳实践与避坑指南
- 优先使用声明式事务:99% 的业务场景都应该使用声明式事务,不要使用编程式事务
- 统一配置
rollbackFor = Exception.class:让所有异常都触发回滚,避免受检异常不回滚的问题 - 避免同类中方法自调用:如果需要调用本类的事务方法,使用代理对象调用
- 不要在事务方法中开启新线程:多线程会导致事务失效
- 合理设置传播行为和隔离级别:根据业务需求选择合适的传播行为和隔离级别
- 事务粒度要尽可能小:不要在事务中执行耗时的操作,比如调用第三方接口、复杂的查询等
- 避免大事务:大事务会导致锁等待时间长、回滚慢、数据库连接占用时间长等问题
- 测试事务是否生效:在开发阶段,一定要测试事务是否按照预期的方式回滚
八、常见误区纠正
-
误区 :
@Transactional注解加在类上,所有方法都会有事务。 纠正:只有 public 方法才会有事务,非 public 方法不会被代理。 -
误区 :只要抛出异常,事务就会回滚。 纠正:Spring 默认只会在抛出 RuntimeException 和 Error 时回滚,受检异常不会回滚。
-
误区 :同类中方法调用,事务会生效。 纠正:自调用是调用原对象的方法,不是代理对象的方法,事务不会生效。
-
误区 :隔离级别越高越好。 纠正:隔离级别越高,并发性能越差。绝大多数场景下,使用数据库默认的隔离级别即可。
-
误区 :事务超时时间是从方法执行开始计算的。 纠正:事务超时时间是从事务开始计算的,也就是从数据库连接获取开始计算的。
九、高频面试题解答
-
问:Spring 事务的底层原理是什么? 答:Spring 事务的底层原理是 AOP 动态代理。当给方法加上
@Transactional注解时,Spring 会为这个类生成一个代理对象,在代理对象的方法执行前后,自动添加事务管理的逻辑。 -
问:
@Transactional注解为什么会失效? 答:常见的失效原因有:方法不是 public 的、同类中方法自调用、异常类型不对、手动捕获异常没有抛出、错误的传播行为、多线程环境、数据库不支持事务等。 -
问:事务的 7 种传播行为分别是什么? 答:7 种传播行为是:REQUIRED、REQUIRES_NEW、NESTED、SUPPORTS、NOT_SUPPORTED、NEVER、MANDATORY。最常用的是 REQUIRED、REQUIRES_NEW 和 NESTED。
-
问:事务的 4 种隔离级别分别是什么?解决了什么问题? 答:4 种隔离级别是:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。分别解决了脏读、不可重复读和幻读问题。
-
问:同类中方法调用为什么会导致事务失效?怎么解决? 答:因为自调用是调用原对象的方法,不是代理对象的方法,不会被 AOP 拦截。解决方案是:注入自己的代理对象、使用 AopContext.currentProxy () 获取代理对象,或者将事务方法放到另一个类中。
-
问:编程式事务和声明式事务有什么区别? 答:编程式事务是通过代码手动管理事务,灵活但代码侵入性强;声明式事务是通过注解或 XML 配置管理事务,使用简单但灵活性稍差。绝大多数场景下推荐使用声明式事务。
十、总结
Spring 事务是 Java 后端开发中最常用的功能之一,也是最容易踩坑的功能之一。很多线上的数据不一致问题,都是因为事务使用不当导致的。
本文从核心原理出发,详细讲解了 Spring 事务的实现机制、@Transactional注解的核心参数、12 种常见的事务失效场景、事务的传播行为和隔离级别,最后给出了最佳实践和避坑指南。
记住:事务的本质是保证数据的一致性。在使用事务时,我们不仅要知道怎么用,更要知道为什么这么用,以及可能会遇到什么问题。只有这样,才能写出健壮、可靠的代码,避免线上事故的发生。