Spring 事务管理与数据访问详解
定位:讲透 Spring 事务抽象、七种传播行为、隔离级别与回滚规则、事务失效全景与 JdbcTemplate 数据访问
适用版本:Spring Framework 6.x(JDK 17+)
说明:本篇侧重事务语义;数据库隔离级别原理归数据库方面的知识
目录
一、事务抽象
1.1 统一事务编程模型
底层事务 API 各不相同(JDBC 的 Connection、JPA 的 EntityTransaction、JTA 的 UserTransaction),Spring 用 PlatformTransactionManager 统一抽象:
PlatformTransactionManager
├── DataSourceTransactionManager ← JDBC/MyBatis
├── JpaTransactionManager ← JPA/Hibernate
└── JtaTransactionManager ← 分布式/XA
业务代码面向统一接口,换持久层技术不改事务代码。
1.2 声明式与编程式
| 方式 | 用法 | 适用 |
|---|---|---|
| 声明式 | @Transactional(AOP 织入,FW-03) |
主流:方法级事务 |
| 编程式 | TransactionTemplate |
需要精确控制范围(如循环内分段提交、只包一小段) |
java
transactionTemplate.execute(status -> {
// 事务范围仅此 lambda
return dao.update(...);
});
TransactionTemplate 常被忽视的价值:把事务范围收到最小------大方法里只有两行需要事务时,用它比整个方法 @Transactional 更合理(长事务是性能杀手)。
1.3 事务定义四要素
传播行为(与已有事务的关系)
隔离级别(并发可见性)
超时(超时自动回滚,防长事务)
只读(只读优化提示)
二、传播行为
传播行为回答:"方法被调用时已有事务怎么办?"七种,按三组记忆:
2.1 支持当前事务(最常用)
| 行为 | 有事务 | 无事务 |
|---|---|---|
| REQUIRED(默认) | 加入 | 新建 |
| SUPPORTS | 加入 | 非事务执行 |
| MANDATORY | 加入 | 抛异常 |
2.2 另起事务/挂起
| 行为 | 行为 |
|---|---|
| REQUIRES_NEW | 挂起当前,新建独立事务(独立提交/回滚) |
| NOT_SUPPORTED | 挂起当前,非事务执行 |
| NEVER | 有事务则抛异常 |
2.3 嵌套
| 行为 | 语义 |
|---|---|
| NESTED | 在当前事务内开保存点(savepoint),嵌套部分可独立回滚,但整体提交仍受外层控制 |
2.4 REQUIRES_NEW 与 NESTED 的辨析(高频)
REQUIRES_NEW:两个独立事务
内层提交后不受外层回滚影响(已独立提交)
典型:操作日志/审计------主业务回滚了日志也要留
NESTED:一个事务 + 保存点
内层回滚只回滚到保存点;外层回滚则一起回滚
外层提交前,内层修改并未真正提交
一句话:独立提交选 REQUIRES_NEW,部分可撤销选 NESTED。
2.5 传播行为的常见坑
SUPPORTS+ 写操作:无事务时没有原子性保证,写操作用它等于裸奔;REQUIRES_NEW内部调用同类方法仍可能自调用失效(还是代理问题);- 只读事务(
readOnly = true)里做写操作,部分实现会拒绝/无优化效果。
三、隔离级别与回滚规则
3.1 隔离级别
默认:沿用数据库默认(MySQL InnoDB 为 REPEATABLE_READ)
显式:@Transactional(isolation = Isolation.READ_COMMITTED)
常见选择:多数互联网业务用 READ_COMMITTED(防脏读,性能与一致性平衡);依赖间隙锁防幻读的场景保持 RR。隔离级别原理(脏读/不可重复读/幻读)归数据库知识。
3.2 回滚规则(必记)
默认:只回滚 RuntimeException 与 Error
受检异常(IOException 等)默认【不回滚】,事务照常提交
定制:
@Transactional(rollbackFor = Exception.class) ← 常用,所有异常都回滚
@Transactional(noRollbackFor = BizWarnException.class) ← 特定异常不回滚
建议默认配置 rollbackFor = Exception.class------"业务失败就回滚"符合直觉,避免受检异常导致的数据不一致。
3.3 事务内异常的正确姿势
要回滚:让异常抛出事务方法(或在 catch 里手动 setRollbackOnly)
要吞掉但要回滚:
try { ... } catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 再返回降级结果
}
吞异常又不标记回滚 = 数据不一致的温床。
四、事务失效场景
汇总:
| 场景 | 根因类别 |
|---|---|
| 同类自调用 | 代理:绕过代理对象 |
| 方法非 public | 代理:默认只拦 public |
| 异常被吞/受检异常未配 | 回滚规则 |
| 静态方法、final 方法 | 代理:拦不到 |
| 数据库引擎不支持(MyISAM) | 基础设施 |
| 传播行为误用(SUPPORTS 写) | 语义误用 |
| 调用方不在 Spring 管理 | 代理:对象不是 Bean |
排查顺序:先看是否经代理(自调用/非 Bean)→ 再看回滚规则 → 再看传播行为与引擎。
五、JdbcTemplate 数据访问
5.1 定位
JDBC 的薄封装:免手动关资源(try-with-resources 由框架处理)、参数绑定防注入、异常翻译。适合简单 SQL、运维脚本、不适合/不想上 ORM 的场景。
5.2 核心用法
java
// 增删改
jdbcTemplate.update("INSERT INTO t_user(name, age) VALUES (?, ?)", name, age);
// 批量
jdbcTemplate.batchUpdate(sql, batchArgs);
// 查询
User u = jdbcTemplate.queryForObject(
"SELECT id, name, age FROM t_user WHERE id = ?",
new BeanPropertyRowMapper<>(User.class), id);
List<User> users = jdbcTemplate.query(
"SELECT id, name, age FROM t_user WHERE age > ?",
new BeanPropertyRowMapper<>(User.class), 18);
5.3 异常翻译
SQLException(受检、厂商相关)
↓ PersistenceExceptionTranslator
DataAccessException 体系(非受检、统一语义)
如 DuplicateKeyException / DeadlockLoserDataAccessException
价值:上层不再写厂商相关的 SQLException 判断,异常语义跨持久层统一。
5.4 与 ORM 的分工
| 场景 | 工具 |
|---|---|
| 领域对象存取、状态管理 | ORM(Hibernate/JPA) |
| 动态 SQL、复杂查询 | MyBatis / jOOQ |
| 简单 SQL、DDL、脚本、统计 | JdbcTemplate |
同一项目可共存(共享 DataSource 与事务管理器),见 ORM 选型篇。
六、总结
- 事务抽象:PlatformTransactionManager 统一 JDBC/ORM/JTA;声明式 @Transactional 为主流,TransactionTemplate 用于把事务范围收到最小。
- 传播行为七种三组:支持当前(REQUIRED 默认/SUPPORTS/MANDATORY)、挂起另起(REQUIRES_NEW/NOT_SUPPORTED/NEVER)、嵌套(NESTED);REQUIRES_NEW 独立提交、NESTED 保存点,是最高频辨析。
- 回滚规则 :默认只回滚运行时异常;受检异常要配
rollbackFor = Exception.class;吞异常必须手动 setRollbackOnly。 - 失效排查序:代理(自调用/非 public/非 Bean)→ 回滚规则 → 传播行为与引擎。
- JdbcTemplate:薄封装、免关资源、异常翻译为 DataAccessException;与 ORM 按场景分工共存。
七、常见高频面试题
1. Spring 的事务抽象是怎么设计的?
要点:PlatformTransactionManager 统一不同底层(DataSourceTransactionManager 管 JDBC/MyBatis,JpaTransactionManager 管 JPA,JtaTransactionManager 管分布式),业务代码面向统一接口,切换持久层不改事务代码。使用分声明式(@Transactional,AOP 织入,主流)与编程式(TransactionTemplate,精确控制范围)。事务定义四要素:传播行为、隔离级别、超时、只读。
2. 详细说说 Spring 的事务传播行为。
要点:七种分三组。支持当前事务:REQUIRED(默认,有则加入无则新建)、SUPPORTS(有则加入无则非事务)、MANDATORY(必须有否则抛异常);挂起另起:REQUIRES_NEW(挂起当前新建独立事务)、NOT_SUPPORTED(挂起当前非事务执行)、NEVER(有事务则抛异常);嵌套:NESTED(保存点)。最常用 REQUIRED 与 REQUIRES_NEW;SUPPORTS 配写操作是典型误用。
3. REQUIRES_NEW 和 NESTED 的区别?
要点:REQUIRES_NEW 是真正的两个独立事务------挂起外层新开事务,内层提交后不受外层回滚影响,典型场景是审计日志(主业务回滚日志也要留)。NESTED 仍是一个事务------在当前事务内开保存点,嵌套部分可独立回滚到保存点,但外层回滚会连它一起回滚,外层提交前内层修改未真正提交。记忆:独立提交选 REQUIRES_NEW,部分可撤销选 NESTED。
4. Spring 事务默认对哪些异常回滚?怎么改?
要点:默认只回滚 RuntimeException 与 Error;受检异常(如 IOException、自定义 extends Exception)默认不回滚,事务照常提交------这是数据不一致的常见来源。定制:@Transactional(rollbackFor = Exception.class) 让所有异常回滚(推荐作为默认姿势),或 noRollbackFor 指定例外。吞异常的场景要手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
5. @Transactional 失效的场景有哪些?
要点:① 同类自调用绕过代理;② 方法非 public;③ 异常被吞或受检异常未配回滚;④ 静态方法、final 方法代理拦不到;⑤ 数据库引擎不支持事务(MyISAM);⑥ 传播行为误用(如 SUPPORTS 配写操作);⑦ 对象不是 Spring Bean。排查顺序:先代理问题(自调用/非 public/非 Bean),再回滚规则,再传播行为与引擎。
6. 什么场景用编程式事务(TransactionTemplate)?
要点:需要精确控制事务范围时。典型:大方法中只有几行需要事务(避免整方法 @Transactional 造成长事务);循环中分段提交(每批一个事务,避免单事务过大);条件性地开启事务。编程式把事务边界显式写进代码,比声明式更灵活可控;但日常业务仍以声明式为主。
7. 长事务有什么危害?如何避免?
要点:长事务持有连接与行锁时间长,导致连接池耗尽、锁等待扩散、主从延迟加大、超时连锁。避免:事务方法内不做远程调用与耗时计算(移出事务或先准备后落库);用 TransactionTemplate 把范围收到最小;拆分大批量为分段提交;设置超时(timeout)兜底。事务边界应与业务原子操作对齐,短而原子。
8. 只读事务(readOnly)有什么作用?
要点:声明事务只读,框架与底层可做优化:如 Hibernate 场景下实体不进脏检查、数据库可能路由到从库或优化锁策略。注意它是"提示"不是强制:只读事务里写数据,不同实现表现不一(报错/忽略/照写);读写混合方法不要标只读。读多场景(报表、查询服务)开启可获得真实收益。
9. JdbcTemplate 相比原生 JDBC 好在哪?
要点:① 免资源管理:连接/语句/结果集的获取与关闭由框架处理;② 参数绑定防 SQL 注入;③ 异常翻译:厂商 SQLException 统一转为 DataAccessException 非受检体系(如 DuplicateKeyException),上层无需厂商判断;④ 提供 RowMapper/BeanPropertyRowMapper 简化映射、batchUpdate 批量。定位是轻封装,不做对象状态管理,与 ORM 按场景分工。
10. 一个事务方法调用另一个事务方法,传播行为如何生效?
要点:传播行为只在"经过代理调用"时生效。外部调用 B 的 @Transactional 方法:B 的代理拦截,按其传播行为决定加入当前事务还是新建(如 REQUIRED 加入、REQUIRES_NEW 挂起新建)。若 A 同类内部调用 B,绕过代理,B 的事务注解完全失效(等于普通方法在 A 的事务里执行)。跨类调用还要注意:B 抛异常回滚后若被 A 捕获不再抛出,A 的事务可能已标脏(同一事务时抛 TransactionSystemException/UnexpectedRollbackException)。
