Spring Framework 事务管理 系统性知识体系
事务管理是企业级应用绕不开的核心话题。很多同学刚接触Spring的时候,觉得事务不就是加个 @Transactional 注解吗?实则不然。从底层原理解析到实际开发中的各种“坑”,这里面的门道远比你想象的多。今天,咱们就把Spring事务管理的整个知识体系彻底梳理一遍。
一、事务管理基础:ACID特性
事务是数据库操作的最小执行单元,它承诺了一个底线——要么全部成功,要么全部失败。而事务的四大基本特性,被合称为ACID,这也是事务管理的理论基石。
| 特性 | 英文全称 | 核心含义 | 实现机制 |
|---|---|---|---|
| 原子性 | Atomicity | 事务是不可分割的整体,所有操作要么全部执行,要么全部回滚 | 数据库的Undo Log(回滚日志)记录数据修改前的状态,异常时回滚到原始状态 |
| 一致性 | Consistency | 事务执行前后,数据库的完整性约束(主键、外键、唯一索引等)不被破坏 | 由原子性、隔离性、持久性共同保证,同时依赖应用层业务逻辑校验 |
| 隔离性 | Isolation | 多个并发事务之间相互隔离,互不干扰 | 数据库通过锁机制和MVCC(多版本并发控制)实现不同的隔离级别 |
| 持久性 | Durability | 事务一旦提交,对数据库的修改就是永久性的,即使系统崩溃也不会丢失 | 数据库的Redo Log(重做日志)记录数据修改后的状态,系统重启时恢复已提交事务 |
二、事务隔离级别
隔离级别解决的是并发事务之间的相互影响问题。你可以把它理解成一个天平:隔离级别越高,数据一致性越好,但并发性能就越低。
2.1 并发事务导致的三大问题
脏读:一个事务读取了另一个事务未提交的修改数据。这就像你看了别人还没定稿的版本,结果对方撤回了,你手里的信息就成了废纸。
不可重复读:同一个事务内,两次读取同一行数据,结果不同。因为中间有其他事务提交了修改。
幻读:同一个事务内,两次执行相同的查询,结果集行数不同。因为中间有其他事务提交了插入或删除操作。
| 问题 | 描述 |
|---|---|
| 脏读(Dirty Read) | 一个事务读取了另一个事务未提交的修改数据 |
| 不可重复读(Non-Repeatable Read) | 同一个事务内,对同一行数据的两次读取结果不一致(因为中间被其他事务修改并提交) |
| 幻读(Phantom Read) | 同一个事务内,两次执行相同的查询语句,第二次查询返回了第一次没有的行(因为中间被其他事务插入了新数据并提交) |
2.2 标准SQL定义的四个隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 数据库默认级别/说明 |
|---|---|---|---|---|
READ_UNCOMMITTED(读未提交) |
✅ | ✅ | ✅ | 最低级别,允许读取未提交的数据,几乎不使用 |
READ_COMMITTED(读已提交) |
❌ | ✅ | ✅ | 大多数数据库的默认级别(Oracle、SQL Server),只能读取已提交的数据 |
REPEATABLE_READ(可重复读) |
❌ | ❌ | ✅ | MySQL InnoDB的默认级别,保证同一个事务内多次读取同一行数据的结果一致 |
SERIALIZABLE(串行化) |
❌ | ❌ | ❌ | 最高级别,强制事务串行执行,完全避免并发问题,但性能极差 |
2.3 Spring中的隔离级别配置
Spring通过 Isolation 枚举定义了5种隔离级别:
public enum Isolation { DEFAULT(-1), // 使用数据库默认隔离级别READ_UNCOMMITTED(1),READ_COMMITTED(2),REPEATABLE_READ(4),SERIALIZABLE(8);}
使用方式很简单:
@Transactional(isolation = Isolation.REPEATABLE_READ)public void updateUser() { // 业务逻辑}
三、事务传播行为
传播行为是Spring框架独有的特性,它定义了当多个事务方法互相调用时,事务该如何在这些方法之间传递。Spring共定义了7种传播行为,可以分为三类。
3.1 核心传播行为详解
1. 支持当前事务的传播行为
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) |
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
SUPPORTS |
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行 |
MANDATORY |
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
2. 不支持当前事务的传播行为
| 传播行为 | 说明 |
|---|---|
REQUIRES_NEW |
总是创建一个新的事务;如果当前存在事务,则将当前事务挂起 |
NOT_SUPPORTED |
总是以非事务方式执行;如果当前存在事务,则将当前事务挂起 |
NEVER |
总是以非事务方式执行;如果当前存在事务,则抛出异常 |
3. 嵌套事务
| 传播行为 | 说明 |
|---|---|
NESTED |
如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务 |
| 传播行为 | 含义 | 存在事务时 | 不存在事务时 | 适用场景 |
|---|---|---|---|---|
| REQUIRED(默认) | 必须有事务 | 加入当前事务 | 新建一个事务 | 绝大多数业务场景 |
| REQUIRES_NEW | 必须有新事务 | 挂起当前事务,新建独立事务 | 新建一个事务 | 日志记录、审计等需要独立提交的操作 |
| SUPPORTS | 支持事务 | 加入当前事务 | 以非事务方式执行 | 查询操作 |
| NOT_SUPPORTED | 不支持事务 | 挂起当前事务,以非事务方式执行 | 以非事务方式执行 | 不需要事务的查询操作,提高性能 |
| MANDATORY | 强制有事务 | 加入当前事务 | 抛出异常 | 必须在事务中执行的方法,防止被误调用 |
| NEVER | 禁止有事务 | 抛出异常 | 以非事务方式执行 | 绝对不能在事务中执行的方法 |
| NESTED | 嵌套事务 | 在当前事务内创建嵌套事务(保存点) | 新建一个事务 | 复杂业务中需要部分回滚的场景 |
3.2 关键传播行为对比
这部分很关键,咱们着重看一下:
REQUIRED vs REQUIRES_NEW
REQUIRED:多个方法共享同一个事务。任何一个方法抛出异常,都会导致整个事务回滚。可以理解成“绑在一根绳上的蚂蚱”。
REQUIRES_NEW:每个方法都有自己独立的事务。内部方法异常不影响外部事务,外部事务异常也不会回滚内部已提交的事务。
NESTED vs REQUIRES_NEW
NESTED:嵌套事务是外部事务的子事务,它依赖保存点(Sa vepoint)实现。外部事务回滚会导致嵌套事务一起回滚;但嵌套事务回滚不会影响外部事务,它只会回滚到自己的保存点。
REQUIRES_NEW:这是完全独立的事务,它们之间没有任何关联。
四、@Transactional 底层原理
@Transactional 注解是Spring声明式事务的王牌,其底层核心就是AOP(面向切面编程)+ 动态袋里。
4.1 整体执行流程
袋里创建:Spring容器启动时,会扫描所有带 @Transactional 注解的类和方法,并为它们创建动态袋里对象。
- 如果目标类实现了接口:使用JDK动态袋里。
- 如果目标类没有实现接口:使用CGLIB动态袋里。
方法调用:当我们调用目标方法时,实际上调用的袋里对象的方法。
- 事务拦截:袋里对象的方法会被
TransactionInterceptor拦截。 - 事务开启:
TransactionInterceptor调用PlatformTransactionManager获取事务状态,开启事务。 - 方法执行:执行目标方法的业务逻辑。
- 事务提交/回滚:方法正常执行,提交事务;方法抛出未被捕获的异常,根据配置判断是否回滚。
4.2 核心组件
PlatformTransactionManager:事务管理器,Spring事务的核心接口,它定义了事务的基本操作。
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;void commit(TransactionStatus status) throws TransactionException;void rollback(TransactionStatus status) throws TransactionException;}
不同持久化框架有不同的实现:
- JDBC/MyBatis:
DataSourceTransactionManager - JPA/Hibernate:
JpaTransactionManager - JTA分布式事务:
JtaTransactionManager
TransactionDefinition:事务定义信息,包含隔离级别、传播行为、超时时间、只读属性等配置。
TransactionStatus:事务状态信息,用于判断事务是否是新事务、是否有保存点等运行时状态。
4.3 事务回滚规则
默认规则:只有当方法抛出运行时异常(RuntimeException) 和错误(Error) 时,才会回滚事务。
自定义回滚规则:
// 指定回滚的异常类型@Transactional(rollbackFor = Exception.class)// 指定不回滚的异常类型@Transactional(noRollbackFor = NullPointerException.class)
五、@Transactional失效场景及解决方案
这是很多开发者容易踩坑的地方。 @Transactional 注解失效,绝大多数都是因为动态袋里机制的局限性导致的。下面是最常见的10种失效场景与解决方案。
1. 方法不是public的
- 失效原因:Spring AOP只能拦截public方法,private、protected、default方法上的
@Transactional注解是无效的。 - 解决方案:将事务方法改为public。
2. 同一个类内部方法调用
- 失效原因:在同一个类中,一个方法调用另一个标注了
@Transactional的方法,实际上是通过this调用目标方法,而不是通过袋里对象,因此不会被事务拦截器拦截。 - 解决方案:
- 注入自己的袋里对象:
@Autowired private UserService userService;,然后通过userService.methodB()调用。 - 使用
AopContext.currentProxy()获取当前袋里对象:((UserService) AopContext.currentProxy()).methodB(); - 将事务方法拆分到不同的类中。
- 注入自己的袋里对象:
3. 异常类型不正确
- 失效原因:Spring默认只对运行时异常(
RuntimeException)和错误(Error)进行回滚,对受检异常(Checked Exception)不回滚。 - 解决方案:
// 对所有异常进行回滚@Transactional(rollbackFor = Exception.class)// 对指定异常进行回滚@Transactional(rollbackFor = { IOException.class, SQLException.class})
4. 异常被捕获但没有重新抛出
- 失效原因:如果在业务方法中捕获了异常但没有重新抛出,事务拦截器会认为方法执行成功,不会触发回滚。
- 解决方案:捕获异常后重新抛出,或者手动设置事务回滚。
@Transactionalpublic void updateUser() { try { // 业务逻辑} catch (Exception e) { // 手动设置事务回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}
5. 数据库引擎不支持事务
- 失效原因:MySQL的MyISAM引擎不支持事务,只有InnoDB引擎支持事务。
- 解决方案:将数据库表的引擎改为InnoDB。
6. 事务传播行为配置错误
- 失效原因:使用了不支持事务的传播行为,如
SUPPORTS、NOT_SUPPORTED、NEVER。 - 解决方案:根据业务需求正确配置传播行为,大多数情况下使用默认的
REQUIRED即可。
7. 多线程环境下
- 失效原因:Spring事务上下文是通过
ThreadLocal绑定到当前线程的,多线程环境下,子线程无法获取父线程的事务上下文。 - 解决方案:避免在事务方法中开启新线程执行数据库操作,或者使用分布式事务。
8. 方法被final或static修饰
- 失效原因:final方法无法被重写,static方法属于类而不是对象,都无法被CGLIB动态袋里。
- 解决方案:将事务方法改为非final、非static的public方法。
9. 没有被Spring容器管理
- 失效原因:如果类没有被
@Component、@Service等注解标注,没有被Spring容器管理,Spring无法为其创建袋里对象。 - 解决方案:将类添加到Spring容器中。
10. 错误的事务管理器
- 失效原因:如果项目中有多个数据源,配置了多个事务管理器,但没有指定使用哪个事务管理器。
- 解决方案:在
@Transactional注解中指定事务管理器的名称。
@Transactional(transactionManager = "userTransactionManager")public void updateUser() { // 业务逻辑}
六、事务管理最佳实践
- 优先使用声明式事务:除非需要细粒度的事务控制,否则优先使用
@Transactional注解。 - 合理设置事务属性:
- 只读事务:对于查询方法,设置
readOnly = true可以提高性能。 - 事务超时:设置
timeout属性,避免事务长时间占用数据库连接。 - 隔离级别:根据业务需求选择合适的隔离级别,不要盲目使用最高级别。
- 只读事务:对于查询方法,设置
- 避免大事务:大事务会占用数据库连接时间过长,影响系统并发性能,应将大事务拆分为多个小事务。
- 正确处理异常:明确指定回滚的异常类型,避免异常被捕获后不回滚的问题。
- 避免在事务方法中执行耗时操作:如网络调用、文件读写等,这些操作会延长事务执行时间。
- 分布式事务:对于跨多个数据源或微服务的事务,应使用分布式事务解决方案,如Seata、XA等。
- 避免方法内部调用事务方法:如果必须调用,使用袋里对象。
- 使用
@Transactional(readOnly = true)优化查询操作:告诉数据库这是只读事务,可以优化查询性能。
Spring Framework 事务管理 面试高频问答卡片
(按面试出现频率排序,核心考点全覆盖)
最高频必考题
Q1:@Transactional注解失效的常见场景有哪些?如何解决?
标准答案:
@Transactional失效绝大多数源于动态袋里机制的局限性,常见10种场景:
- 方法不是public的。原因:Spring AOP只能拦截public方法。解决:将事务方法改为public。
- 同一个类内部方法调用。原因:通过
this调用目标方法,不是通过袋里对象,不会被事务拦截器拦截。解决:①注入自己的袋里对象调用;②使用AopContext.currentProxy()获取袋里对象;③拆分到不同类。 - 异常类型不正确。原因:默认只回滚
RuntimeException和Error,受检异常(如IOException)不回滚。解决:添加@Transactional(rollbackFor = Exception.class)。 - 异常被捕获但没有重新抛出。原因:事务拦截器认为方法执行成功,不会触发回滚。解决:①捕获后重新抛出;②手动设置回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。 - 数据库引擎不支持事务。原因:MySQL的MyISAM引擎不支持事务。解决:将表引擎改为InnoDB。
- 事务传播行为配置错误。原因:使用了
SUPPORTS、NOT_SUPPORTED、NEVER等不支持事务的传播行为。解决:大多数业务场景使用默认的REQUIRED即可。 - 多线程环境下。原因:Spring事务上下文通过
ThreadLocal绑定到当前线程,子线程无法获取。解决:避免在事务方法中开启新线程执行数据库操作,或使用分布式事务。 - 方法被final或static修饰。原因:final方法无法被重写,static方法属于类,都无法被CGLIB动态袋里。解决:将事务方法改为非final、非static的public方法。
- 类没有被Spring容器管理。原因:没有添加
@Component、@Service等注解,Spring无法创建袋里对象。解决:将类添加到Spring容器中。 - 多个事务管理器未指定。原因:项目中有多个数据源和事务管理器,Spring不知道使用哪个。解决:在
@Transactional中指定transactionManager属性。
Q2:Spring事务的传播行为是什么?有哪几种?分别解释一下?
标准答案:
传播行为是Spring框架独有的特性,定义了多个事务方法相互调用时,事务如何在这些方法之间传播的规则。共7种,分为三类:
支持当前事务:
- REQUIRED(默认):有事务就加入,没有就新建。适用于绝大多数业务场景。
- SUPPORTS:有事务就加入,没有就以非事务方式执行。适用于查询操作。
- MANDATORY:有事务就加入,没有就抛出异常。用于强制必须在事务中执行的方法。
不支持当前事务:
- REQUIRES_NEW:总是新建独立事务,有事务就挂起当前事务。适用于日志、审计等需要独立提交的操作。
- NOT_SUPPORTED:总是以非事务方式执行,有事务就挂起。适用于不需要事务的查询,提高性能。
- NEVER:总是以非事务方式执行,有事务就抛出异常。用于绝对不能在事务中执行的方法。
嵌套事务:
- NESTED:有事务就创建嵌套事务(保存点),没有就新建事务。适用于需要部分回滚的复杂业务场景。
Q3:REQUIRED和REQUIRES_NEW传播行为的区别是什么?
标准答案:
核心区别在于事务的独立性:
- REQUIRED:多个方法共享同一个事务。任何一个方法抛出异常,都会导致整个事务回滚。
- REQUIRES_NEW:每个方法都有自己独立的事务。内部方法异常不会影响外部事务,外部事务异常也不会回滚内部已提交的事务。
举例:方法A调用方法B:
- B使用REQUIRED:A和B在同一个事务中,B异常会导致A和B都回滚;A异常也会导致A和B都回滚。
- B使用REQUIRES_NEW:A和B是两个独立事务,B异常只会回滚B;A异常只会回滚A,不会影响已提交的B。
高频核心题
Q4:什么是事务?事务的ACID特性分别是什么?
标准答案:
事务是数据库操作的最小执行单元,保证一系列操作要么全部成功执行,要么全部失败回滚。ACID是事务的四大基本特性:
- 原子性(Atomicity):事务是不可分割的整体,所有操作要么全部执行,要么全部回滚。通过数据库的Undo Log(回滚日志)实现。
- 一致性(Consistency):事务执行前后,数据库的完整性约束(主键、外键、唯一索引等)不被破坏。由原子性、隔离性、持久性共同保证,同时依赖应用层业务逻辑校验。
- 隔离性(Isolation):多个并发事务之间相互隔离,互不干扰。通过数据库的锁机制和MVCC(多版本并发控制)实现不同的隔离级别。
- 持久性(Durability):事务一旦提交,对数据库的修改就是永久性的,即使系统崩溃也不会丢失。通过数据库的Redo Log(重做日志)实现。
Q5:并发事务会导致哪些问题?分别解释一下?
标准答案:
并发事务执行时,如果没有足够的隔离性,会导致以下三类问题:
- 脏读:一个事务读取了另一个事务未提交的修改数据。如果另一个事务回滚,读取到的数据就是无效的。
- 不可重复读:同一个事务内,两次读取同一行数据,结果不同。因为中间有其他事务对该行数据进行了修改并提交。
- 幻读:同一个事务内,两次执行相同的查询语句,结果集行数不同。因为中间有其他事务执行了插入或删除操作并提交。
Q6:标准SQL定义了哪四个事务隔离级别?分别能解决哪些问题?
标准答案:
标准SQL定义了四个从低到高的隔离级别,级别越高,数据一致性越好,但并发性能越低:
- READ_UNCOMMITTED(读未提交):最低级别,允许读取未提交的数据。不能解决任何并发问题。
- READ_COMMITTED(读已提交):只能读取已提交的数据。解决了脏读问题。是Oracle、SQL Server等大多数数据库的默认级别。
- REPEATABLE_READ(可重复读):保证同一个事务内多次读取同一行数据的结果一致。解决了脏读和不可重复读问题。
- SERIALIZABLE(串行化):最高级别,强制事务串行执行。解决了脏读、不可重复读和幻读所有问题,但性能极差。
Q7:MySQL InnoDB的默认隔离级别是什么?它是如何解决幻读问题的?
标准答案:
MySQL InnoDB引擎的默认隔离级别是REPEATABLE_READ(可重复读)。
标准SQL的可重复读级别无法解决幻读问题,但InnoDB通过Next-Key Lock(临键锁) 机制解决了幻读。Next-Key Lock是行锁 + 间隙锁的组合:
- 行锁:锁定已经存在的记录行。
- 间隙锁:锁定记录之间的间隙,防止其他事务在间隙中插入新数据。
通过临键锁,InnoDB在可重复读级别下就可以避免幻读问题,这是MySQL对标准SQL的重要扩展。
Q8:@Transactional注解的底层实现原理是什么?
标准答案:
@Transactional注解是Spring声明式事务的核心,其底层基于AOP(面向切面编程) 和动态袋里实现,整体执行流程如下:
- 袋里创建:Spring容器启动时,扫描所有带有@Transactional注解的类和方法,为其创建动态袋里对象。
- 目标类实现了接口:使用JDK动态袋里。
- 目标类没有实现接口:使用CGLIB动态袋里。
- 方法拦截:当调用目标方法时,实际调用的是袋里对象的方法,会被
TransactionInterceptor(事务拦截器)拦截。 - 事务开启:事务拦截器调用
PlatformTransactionManager(事务管理器)获取事务状态,开启事务。 - 方法执行:执行目标方法的业务逻辑。
- 事务提交/回滚:方法正常执行完成:提交事务;方法抛出未被捕获的异常:根据配置判断是否回滚。
常考基础题
Q9:NESTED和REQUIRES_NEW传播行为的区别是什么?
标准答案:
核心区别在于与外部事务的关联关系:
- NESTED(嵌套事务):是外部事务的子事务,依赖于外部事务。
- 外部事务回滚:会导致嵌套事务一起回滚。
- 嵌套事务回滚:只会回滚到自己的保存点,不会影响外部事务。
- REQUIRES_NEW(新建事务):是完全独立的事务,与外部事务没有任何关联。
- 外部事务回滚:不会影响内部已提交的事务。
- 内部事务回滚:不会影响外部事务。
Q10:Spring事务管理的核心组件有哪些?分别有什么作用?
标准答案:
Spring事务管理有三个核心接口:
- PlatformTransactionManager(事务管理器):Spring事务的核心接口,定义了事务的基本操作(获取事务、提交、回滚)。不同持久化框架有不同的实现:
- JDBC/MyBatis:
DataSourceTransactionManager - JPA/Hibernate:
JpaTransactionManager - 分布式事务:
JtaTransactionManager
- JDBC/MyBatis:
- TransactionDefinition(事务定义):包含事务的所有配置信息,如隔离级别、传播行为、超时时间、只读属性等。
- TransactionStatus(事务状态):记录事务的运行状态,如是否是新事务、是否有保存点、是否已标记回滚等。
Q11:@Transactional注解默认的回滚规则是什么?如何自定义?
标准答案:
- 默认回滚规则:只有当方法抛出运行时异常(
RuntimeException) 和错误(Error) 时,才会回滚事务。对于受检异常(Checked Exception,如IOException、SQLException等),默认不会回滚。 - 自定义回滚规则:通过两个属性配置:
rollbackFor:指定需要回滚的异常类型。noRollbackFor:指定不需要回滚的异常类型。
@Transactional(rollbackFor = Exception.class) // 对所有异常回滚
@Transactional(noRollbackFor = NullPointerException.class)
Q12:Spring事务管理有哪些最佳实践?
标准答案:
- 优先使用声明式事务:除非需要细粒度的事务控制,否则优先使用@Transactional注解。
- 合理设置事务属性:
- 查询方法设置
readOnly = true,提高数据库性能。 - 设置合理的
timeout超时时间,避免事务长时间占用连接。 - 根据业务需求选择合适的隔离级别,不要盲目使用最高级别。
- 查询方法设置
- 避免大事务:大事务会占用连接时间过长,影响并发性能,应拆分为多个小事务。
- 正确处理异常:明确指定
rollbackFor属性,避免异常被捕获后不回滚。 - 避免在事务中执行耗时操作:如网络调用、文件读写等,会延长事务执行时间。
- 避免内部调用事务方法:如果必须调用,使用袋里对象。
- 分布式事务场景:跨数据源或微服务的事务,使用Seata等分布式事务解决方案。
