Spring 事务失效 ⭐⭐⭐⭐⭐
面试频率:★★★★★
工作频率:★★★★★
⚡30 秒速记
🟢 一句话
Spring 声明式事务主要依赖 AOP 代理。判断事务为什么失效,首先看方法调用有没有经过 Spring 事务代理,其次看异常是否满足回滚规则。
常见失效场景:
1. 同类内部调用
2. private 方法
3. 对象不是 Spring Bean
4. 异常被 catch 吃掉
5. 异常不满足回滚规则
6. 底层资源不支持事务
7. 多线程 / @Async 导致事务上下文不在同一线程
核心判断:
@Transactional
↓
对象是不是Spring Bean?
↓
调用有没有经过代理?
↓
事务拦截器有没有执行?
↓
异常有没有抛出去?
↓
异常是否满足回滚规则?
↓
底层资源是否支持事务?
一、@Transactional 为什么能实现事务? ⭐⭐⭐⭐⭐
例如:
@Transactional
public void save() {
insertOrder();
}
不是因为:
@Transactional 注解自己会开启事务
而是 Spring 通过:
AOP
+
动态代理
实现。
大致流程:
调用者
↓
Spring代理对象
↓
事务拦截器
↓
开启事务
↓
调用目标方法
↓
是否发生需要回滚的异常?
↙ ↘
没有 有
↓ ↓
commit rollback
所以:
@Transactional 能不能生效,和 Spring 代理有很大关系。
二、失效场景1:同类内部调用 ⭐⭐⭐⭐⭐
这是最经典的事务失效场景之一。
例如:
@Service
public class OrderService {
public void createOrder() {
save();
}
@Transactional
public void save() {
// 保存订单
}
}
看起来:
createOrder()
↓
save()
↓
@Transactional
↓
开启事务
实际上:
save();
相当于:
this.save();
属于:
同一个对象内部调用。
正常事务调用:
Controller
↓
OrderService代理对象
↓
事务拦截器
↓
save()
↓
原始OrderService
但是自调用:
Controller
↓
OrderService代理对象
↓
createOrder()
↓
原始OrderService
↓
this.save()
注意:
this.save()
没有重新经过:
OrderService代理对象
所以:
事务拦截器没有机会拦截save()
导致:
@Transactional
可能不生效。
三、怎么解决同类调用?
比较常见的方式:
把事务方法拆到另外一个 Spring Bean。
例如:
@Service
public class OrderTransactionService {
@Transactional
public void save() {
// 保存订单
}
}
然后:
@Service
public class OrderService {
@Autowired
private OrderTransactionService transactionService;
public void createOrder() {
transactionService.save();
}
}
执行:
OrderService
↓
OrderTransactionService代理对象
↓
事务拦截器
↓
开启事务
↓
save()
这样:
调用经过 Spring 代理。
事务可以正常生效。
四、失效场景2:private 方法 ⭐⭐⭐⭐⭐
例如:
@Transactional
private void save() {
}
这种写法:
不要指望普通 Spring 声明式事务代理按预期生效。
因为:
@Transactional
↓
依赖AOP代理拦截方法
↓
private方法不能像普通可代理方法一样被代理增强
尤其 CGLIB:
通过:
继承目标类
↓
生成代理子类
↓
重写方法
实现代理。
但是:
private方法
子类不能重写。
所以:
无法像普通 public 方法一样进行代理增强。
五、失效场景3:自己 new 对象 ⭐⭐⭐⭐⭐
例如:
OrderService orderService =
new OrderService();
orderService.save();
即使:
@Transactional
public void save() {
}
也不会因为这个注解自动产生 Spring 事务。
原因:
new OrderService()
↓
普通Java对象
↓
不是Spring容器管理的代理Bean
↓
没有事务拦截器
所以:
自己new的对象
≠
Spring容器里的Bean / 代理对象
正常情况:
@Service
public class OrderService {
}
然后让 Spring 注入:
@Autowired
private OrderService orderService;
此时:
Spring容器
↓
OrderService Bean
↓
可能经过AOP代理
↓
@Transactional生效
六、失效场景4:异常被 catch 吃掉 ⭐⭐⭐⭐⭐
这个工作中非常容易踩坑。
例如:
@Transactional
public void createOrder() {
try {
saveOrder();
int i = 1 / 0;
} catch (Exception e) {
log.error("创建订单失败", e);
}
}
执行:
开启事务
↓
saveOrder()
↓
发生异常
↓
catch捕获
↓
没有继续抛出
↓
方法正常结束
事务拦截器看到:
方法正常返回
可能就会:
commit
而不是:
rollback
七、为什么 catch 会影响回滚?
事务代理大致:
开启事务
↓
try {
调用业务方法
commit
} catch {
rollback
}
但是业务方法内部:
try {
// 异常
} catch (Exception e) {
// 吃掉异常
}
导致代理层:
根本没有收到异常
所以:
无法根据这个异常触发预期回滚。
八、怎么解决异常被吃掉?
如果业务要求:
发生异常
↓
整个事务回滚
可以:
记录日志以后继续抛出:
@Transactional
public void createOrder() {
try {
saveOrder();
} catch (Exception e) {
log.error("创建订单失败", e);
throw e;
}
}
流程:
业务发生异常
↓
catch
↓
记录日志
↓
重新throw
↓
异常传到事务代理
↓
rollback
九、失效场景5:异常不满足回滚规则 ⭐⭐⭐⭐⭐
很多人认为:
@Transactional
↓
任何Exception
↓
全部回滚
这是不准确的。
Spring 默认情况下主要对:
RuntimeException
Error
进行回滚。
例如:
@Transactional
public void save() throws Exception {
throw new Exception("失败");
}
这里:
Exception
属于 checked exception。
默认规则下:
不一定按照你想象的方式回滚。
十、rollbackFor ⭐⭐⭐⭐⭐
实际项目非常常见:
@Transactional(
rollbackFor = Exception.class
)
public void save() throws Exception {
}
意思:
Exception及其子类
↓
发生异常
↓
按照配置进行回滚
所以很多项目统一写:
@Transactional(
rollbackFor = Exception.class
)
避免 checked exception 没有触发预期回滚。
十一、RuntimeException 和 Exception
简单记:
Exception
│
├── RuntimeException
│
└── 其他Checked Exception
Spring 默认:
RuntimeException
↓
回滚
Error
↓
回滚
而普通 checked exception:
默认不回滚
如果需要:
rollbackFor = Exception.class
十二、失效场景6:底层资源不支持事务
Spring:
@Transactional
只是负责:
事务管理
最终:
真正的数据提交和回滚:
还需要底层资源支持事务。
例如数据库存储引擎:
支持事务
↓
rollback才有意义
如果:
底层资源本身不支持事务
那么:
Spring想rollback
↓
底层无法真正回滚
十三、失效场景7:多线程 ⭐⭐⭐⭐⭐
这个也非常重要。
Spring 数据库事务通常:
和当前线程绑定。
例如:
@Transactional
public void createOrder() {
saveOrder();
new Thread(() -> {
updateStock();
}).start();
}
执行:
主线程
↓
事务A
↓
saveOrder()
但是:
新线程
↓
updateStock()
新线程:
不会自动继承主线程的数据库事务上下文。
所以:
主线程事务A
≠
新线程事务
十四、@Async 和事务
例如:
@Transactional
public void createOrder() {
asyncService.updateStock();
}
而:
@Async
public void updateStock() {
}
updateStock():
通常会在线程池的另一个线程执行。
所以:
createOrder
↓
线程A
↓
事务A
而:
@Async updateStock
↓
线程B
线程B:
不会自动加入线程A的数据库事务。
所以不要想当然认为:
@Transactional方法
↓
里面所有异步代码
↓
都是同一个事务
不是。
十五、为什么事务和线程有关?
Spring 常见事务管理会把:
数据库连接
事务状态
事务资源
和:
当前线程
关联起来。
可以粗略理解:
Thread A
↓
Connection A
↓
Transaction A
换线程:
Thread B
↓
没有Thread A的事务上下文
所以:
事务不会自动跨线程传播
十六、和事务传播机制不要搞混 ⭐⭐⭐⭐⭐
你刚学过:
REQUIRED
REQUIRES_NEW
例如:
A方法
↓
B方法
如果:
同一个线程
+
调用经过Spring代理
事务传播机制才会决定:
B加入A事务?
还是
B创建新事务?
但是:
如果:
A在线程1
B在线程2
那就不是简单的:
REQUIRED加入事务A
了。
因为:
事务上下文通常没有跨线程过去。
十七、事务失效统一理解 ⭐⭐⭐⭐⭐
不要死背:
10种事务失效场景
以后看到事务问题:
按照下面顺序排查。
第一步:是不是 Spring Bean?
对象是不是Spring创建的?
↓
自己new的?
↓
如果自己new → 没有Spring代理
第二步:有没有经过代理?
外部调用?
还是
this.xxx()?
如果:
this.xxx()
要警惕:
自调用绕过代理
第三步:方法能不能正常被代理?
例如:
private
final(特别是基于子类代理时)
要考虑:
代理限制。
第四步:异常有没有抛出去?
发生异常
↓
是不是catch掉了?
↓
有没有重新throw?
第五步:异常满足回滚规则吗?
RuntimeException?
Error?
Checked Exception?
rollbackFor配置了吗?
第六步:是不是换线程了?
new Thread
@Async
线程池
事务上下文:
通常不会自动跨线程传播
第七步:底层支持事务吗?
例如:
数据库 / 存储引擎
是否真正支持:
commit
rollback
十八、事务失效排查图 ⭐⭐⭐⭐⭐
@Transactional不生效
↓
是不是Spring Bean?
↓ ↓
否 是
↓ ↓
没有代理 是否经过代理?
↓
┌──────┴──────┐
↓ ↓
没有 有
↓ ↓
检查自调用 方法是否可代理?
↓
异常是否抛出?
↓
是否满足回滚规则?
↓
是否切换线程?
↓
底层是否支持事务?
十九、和之前知识串起来 ⭐⭐⭐⭐⭐
现在:
@Transactional
↓
Spring AOP
↓
动态代理
↓
事务拦截器
↓
事务传播机制
↓
数据库事务
所以:
Spring AOP
学懂以后:
事务失效很多问题都不需要死背。
例如:
this.save()
↓
绕过代理
↓
@Transactional失效
以及:
REQUIRES_NEW
↓
需要事务代理识别传播行为
↓
this.save()
↓
绕过代理
↓
REQUIRES_NEW可能无法按预期生效
二十、几个经典面试代码题 ⭐⭐⭐⭐⭐
题目1
public void A() {
B();
}
@Transactional
public void B() {
}
问:
B有事务吗?
答:
如果属于同一个 Bean 的内部调用,B 的 @Transactional 可能因为自调用绕过代理而不生效。
题目2
@Transactional
public void A() {
try {
insert();
throw new RuntimeException();
} catch (Exception e) {
}
}
问:
会回滚吗?
答:
异常被内部 catch 后没有继续抛出,事务代理可能认为方法正常结束,因此不会因为这个异常按预期触发回滚。
题目3
@Transactional
public void A() throws Exception {
insert();
throw new Exception();
}
问:
默认一定回滚吗?
答:
不一定。普通 checked exception 默认不属于 Spring 声明式事务的回滚范围,可以使用 rollbackFor = Exception.class 指定。
题目4
@Transactional
public void A() {
new Thread(() -> {
update();
}).start();
}
问:
update 和 A 是同一个事务吗?
答:
通常不是。Spring 事务上下文通常绑定在线程上,新线程不会自动继承原线程事务。
🎤 面试回答(30秒)
Spring 声明式事务主要基于 AOP 代理实现,所以事务失效首先要考虑方法调用是否经过 Spring 事务代理。
常见场景包括同类内部自调用绕过代理、private 等方法无法按预期代理、对象自己 new 没有交给 Spring 管理、异常被 catch 后没有继续抛出、异常不满足默认回滚规则,以及多线程或 @Async 导致事务上下文不在同一个线程。
排查事务问题时,我一般会从 Spring Bean、代理调用、异常传播、回滚规则和线程这几个方面检查。
🎯 面试追问
Q1:Spring事务为什么会失效?
答:
核心原因通常从:
代理
异常
线程
底层事务支持
几个方面排查。
Q2:为什么同类内部调用事务可能失效?
答:
因为:
this.save();
没有重新经过 Spring 代理对象。
所以:
事务拦截器无法拦截这个方法调用。
Q3:为什么 private 方法不建议加 @Transactional?
答:
因为 Spring 声明式事务依赖 AOP 代理,private 方法不能像普通可代理方法一样被代理增强。
Q4:为什么自己 new 的对象事务不生效?
答:
因为:
new OrderService()
只是普通 Java 对象。
不是:
Spring容器提供的代理Bean
所以:
没有事务拦截器。
Q5:为什么 catch 异常可能导致事务不回滚?
答:
因为:
异常被业务代码吃掉
↓
没有传播到事务代理
↓
代理认为方法正常结束
所以不会因为这个异常按预期触发回滚。
Q6:Spring 默认什么异常回滚?
答:
默认主要对:
RuntimeException
Error
进行回滚。
普通 checked exception:
默认不回滚。
Q7:rollbackFor = Exception.class 有什么作用?
答:
@Transactional(
rollbackFor = Exception.class
)
表示:
Exception 及其子类都按照该配置参与事务回滚判断。
Q8:为什么 @Async 可能导致事务问题?
答:
因为:
@Async
↓
切换线程
而 Spring 数据库事务上下文通常:
绑定当前线程
所以异步线程不会自动加入调用线程已有的事务。
Q9:事务传播和多线程是一回事吗?
答:
不是。
事务传播主要解决:
事务方法A
↓
调用事务方法B
↓
B如何处理当前事务
通常是在同线程调用链中讨论。
多线程:
线程A
↓
线程B
事务上下文通常不会自动跨线程传播。
⚠️ 工作踩坑
1. 看到 @Transactional 就认为一定生效
错误。
应该检查:
是不是Spring Bean?
↓
有没有经过代理?
↓
有没有自调用?
↓
异常有没有抛出?
↓
异常是否满足回滚规则?
↓
有没有切换线程?
2. catch 以后什么都不做
例如:
try {
save();
} catch (Exception e) {
log.error("error", e);
}
如果业务要求整个事务回滚:
不要把异常吃掉后直接正常返回。
3. REQUIRES_NEW 同类调用
例如:
@Transactional
public void A() {
B();
}
@Transactional(
propagation = Propagation.REQUIRES_NEW
)
public void B() {
}
如果:
A和B在同一个Bean
那么:
B()
↓
this.B()
↓
绕过代理
REQUIRES_NEW 可能不会按预期生效。
比较常见的处理:
把B拆到另一个Spring Bean
4. @Async 里认为还能共享外层事务
错误。
线程A:事务A
@Async
↓
线程B
线程B:
通常不会自动共享线程A的事务。
❓ 自测
-
@Transactional 底层为什么依赖 AOP?
-
判断事务失效首先应该想到什么?
-
为什么同类内部调用可能导致事务失效?
-
this.save() 为什么会绕过代理?
-
private 方法为什么不适合普通 Spring 声明式事务?
-
自己 new 的 Service 为什么事务不生效?
-
为什么异常被 catch 后可能不回滚?
-
catch 后重新 throw 为什么可以触发事务代理的回滚判断?
-
Spring 默认哪些异常会回滚?
-
checked exception 默认一定回滚吗?
-
rollbackFor = Exception.class 有什么作用?
-
为什么 @Async / new Thread 会影响事务?
-
Spring 数据库事务为什么和线程有关?
-
REQUIRES_NEW 为什么也可能因为自调用失效?
-
事务传播和多线程事务有什么区别?
-
排查 @Transactional 不生效,你会从哪些方面检查?