事务管理:银行转账不能转一半
转账这个动作,本质是两条 SQL:A 扣款、B 加钱。如果 A 扣完钱、B 还没加钱时服务器宕机,钱就凭空消失了。事务的意义就是:这两步要么都成功,要么都回滚。Spring 把事务分成两种玩法——编程式(手写代码控制)和声明式(贴个 @Transactional 注解),后者是日常主力。
论编程式 vs 声明式
编程式事务:用 TransactionTemplate 或 PlatformTransactionManager 手动写 begin/commit/rollback。控制力强,但代码侵入业务。
声明式事务:在方法上贴一个 @Transactional,Spring 用 AOP 自动包裹事务。业务代码干净,这是 99% 场景的选择。
底层:不管哪种写法,最终都通过 PlatformTransactionManager 接口跟数据库(或消息中间件)打交道。换数据库实现,接口不变。
@Transactional 的六个关键参数
| 参数 | 含义 | 常用值 |
|---|---|---|
propagation | 传播行为:方法被调用时,事务该怎么和已有事务共存。 | REQUIRED(默认)、REQUIRES_NEW、NESTED 等 7 种。 |
isolation | 隔离级别:并发事务之间能看见多少对方的中间状态。 | DEFAULT、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。 |
readOnly | 是否只读。只读事务可以让数据库优化执行计划。 | 查询方法标 true。 |
timeout | 事务超时时间(秒),超时自动回滚。 | 默认 -1(不超时)。 |
rollbackFor | 遇到哪些异常时回滚。 | 默认只对 RuntimeException 和 Error 回滚;checked 异常要写 rollbackFor = Exception.class。 |
noRollbackFor | 遇到哪些异常不回滚。 | 极少数业务需要。 |
七种传播行为,背这四个就够
| 传播行为 | 大白话 |
|---|---|
| REQUIRED(默认) | 有事务就加入,没事务就新建。绝大多数方法用这个。 |
| REQUIRES_NEW | 不管有没有事务,都新开一个独立事务,挂起当前事务。比如主业务要记日志,日志失败不影响主业务——日志方法标 REQUIRES_NEW。 |
| NESTED | 嵌套事务:在当前事务里开一个"保存点",内层失败只回滚到保存点,不影响外层。 |
| SUPPORTS | 有事务就用,没事务就非事务运行。适合查询方法。 |
声明式事务:贴个注解就完事
REQUIRES_NEW 实战:主业务失败不影响审计日志
编程式事务:TransactionTemplate 手动控制(需要精细控制时用)
事务为什么会失效:六个高频翻车现场
@Transactional 贴了却不回滚,是线上最经典的事故之一。原因几乎都和"代理没生效"有关。对照自查:
| 失效场景 | 原因 / 解法 |
|---|---|
| 1. 方法不是 public | Spring 代理只拦 public 方法,protected/private 上的 @Transactional 直接被忽略。 |
| 2. 同类内部调用 | this.method() 不走代理对象,@Transactional 不生效。要让"被调方法"从另一个 Bean 进来,或注入自己。 |
| 3. 异常被自己 catch 了 | try-catch 吞了异常没抛出去,Spring 不知道出错,不回滚。要么重新抛,要么手动 setRollbackOnly()。 |
| 4. 抛的是 checked 异常 | 默认只对 RuntimeException/Error 回滚。抛 IOException 不回滚,要配 rollbackFor=Exception.class。 |
| 5. 类没被 Spring 管理 | 自己 new 的对象不走代理,@Transactional 当然无效。必须交给容器注入。 |
| 6. 数据库引擎不支持事务 | MySQL 用 MyISAM(不支持事务)而不是 InnoDB,贴啥注解都没用。 |
完整可运行案例:银行转账全链路(含隔离级别与回滚规则)
把上面的零件拼成一个完整工程:一个 transfer 方法同时演示 传播行为 REQUIRED、隔离级别、rollbackFor 回滚规则。重点看转账到一半抛异常时,数据库里的钱是怎么被"退回去"的。
转账服务:传播行为 + 隔离级别 + 回滚规则一口气配齐
DAO 层(JdbcTemplate 操作 H2 内存库)
① 同类内部调用:this.transfer() 不经过代理,注解直接被忽略。把调用方拆到另一个 Bean。
② 方法不是 public:private/protected 方法上贴 @Transactional,不生效。
③ 异常被你自己 catch 了:方法内部 try-catch 把异常吞掉,Spring 根本不知道出错了,当然不会回滚。想回滚就得在 catch 里抛出去,或手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
④ 异常类型不对:默认只对 RuntimeException 回滚。你抛了个 IOException(checked),事务不回滚。记得写 rollbackFor = Exception.class。
⑤ 类没被 Spring 管理:自己 new 出来的对象,事务注解等于空气。
1.(高频题)@Transactional 的 propagation 里,REQUIRED 和 REQUIRES_NEW 有什么区别?分别什么时候用?
查看答案
答案:REQUIRED(默认):有事务就加入,没事务就新建,多个方法共用一个事务。REQUIRES_NEW:不管外层有没有事务,都挂起外层、新开独立事务,内层提交不影响外层、外层回滚也不影响内层。解析:典型场景:主业务写订单,旁边要记一条"审计日志"——日志方法标 REQUIRES_NEW,主业务挂了日志也得留着。
2.(排错题)方法上贴了 @Transactional,里面抛了 IOException,结果事务没回滚。为什么?怎么修?
查看答案
答案:@Transactional 默认只对 RuntimeException 和 Error 回滚。IOException 是 checked 异常,默认不回滚。解析:修法:写 @Transactional(rollbackFor = Exception.class),把回滚范围扩大到所有异常。这是生产代码的标配写法。
3.(理解题)数据库事务的四个隔离级别,分别解决什么并发问题?
查看答案
答案:读未提交(会脏读)→ 读已提交(防脏读,Oracle/PG 默认)→ 可重复读(防脏读+不可重复读,MySQL InnoDB 默认)→ 串行化(全防,但性能最差)。解析:脏读=读到别人没提交的数据;不可重复读=同一事务内两次读同一行结果不一样;幻读=两次范围查询行数对不上。
4.(陷阱题)@Transactional 写在 private 方法上,为什么不生效?
查看答案
答案:Spring 事务靠 AOP 动态代理实现,而 JDK 动态代理基于接口、CGLIB 基于子类覆写 public 方法,private 方法根本无法被代理覆盖。解析:事务注解只能贴在 public 方法上。这是 AOP 体系的通病,@Transactional 和 @Async 都一样。