Spring事务为什么会失效?常见场景与解决方案总结
在 Spring 开发中,很多开发者认为:
java@Transactional加上事务注解后,事务一定会生效。
但实际开发中经常出现:
- 数据已经提交,事务却没有回滚
- 方法明明加了
@Transactional却没有开启事务- 多线程场景事务失效
- try-catch 后事务不回滚
本文结合 Spring 源码原理,总结事务失效的常见场景以及解决方案。
目录
- Spring事务简介
- Spring事务实现原理
- Transactional执行流程
- ThreadLocal与事务
- 事务失效的常见场景
- 非public方法
- 同类内部调用
- 异常被捕获
- 抛出受检异常
- 多线程事务失效
- 手动new对象
- 数据库引擎不支持事务
- 多线程事务失效深度解析
- 解决方案
- 面试高频问题
- 总结
一、Spring事务简介
Spring事务的核心注解:
java
@Transactional
示例:
java
@Service
public class UserService {
@Transactional
public void createUser() {
userMapper.insert(user);
orderMapper.insert(order);
}
}
作用:
text
保证多个数据库操作
要么全部成功
要么全部失败
二、Spring事务实现原理
很多人以为:
text
@Transactional
=
数据库事务
实际上:
text
@Transactional
=
AOP + 数据库事务
Spring会为目标对象创建代理对象:
text
UserService
↓
TransactionProxy
↓
真实UserService
调用流程:
text
调用代理对象
↓
开启事务
↓
执行目标方法
↓
提交事务
或
回滚事务
三、Transactional执行流程
例如:
java
@Transactional
public void save() {
}
执行流程:
text
进入代理对象
↓
begin transaction
↓
执行save()
↓
commit
↓
结束
如果发生异常:
text
begin transaction
↓
执行save()
↓
exception
↓
rollback
四、ThreadLocal与事务
Spring事务核心依赖:
java
ThreadLocal
源码中:
java
TransactionSynchronizationManager
内部维护:
java
private static final ThreadLocal<Map<Object,Object>>
作用:
text
保存当前线程事务上下文
例如:
text
线程A
↓
数据库连接A
↓
事务A
text
线程B
↓
数据库连接B
↓
事务B
因此:
text
事务和线程绑定
这是后面多线程事务失效的根本原因。
五、事务失效场景一:非public方法
错误示例:
java
@Transactional
private void saveUser() {
}
原因:
Spring默认基于代理实现事务。
代理只能拦截:
java
public
方法。
正确写法:
java
@Transactional
public void saveUser() {
}
六、事务失效场景二:同类内部调用
这是最经典面试题。
示例:
java
@Service
public class UserService {
public void test() {
saveUser();
}
@Transactional
public void saveUser() {
}
}
问题:
java
test()
调用:
java
saveUser()
事务失效。
为什么?
正常事务流程:
text
代理对象
↓
saveUser()
↓
事务生效
内部调用:
text
this.saveUser()
绕过代理。
直接调用目标对象。
结果:
text
事务失效
解决方案:
通过代理调用:
java
@Autowired
private UserService userService;
public void test() {
userService.saveUser();
}
七、事务失效场景三:异常被捕获
错误示例:
java
@Transactional
public void save() {
try {
int a = 1 / 0;
} catch (Exception e) {
log.error("error");
}
}
结果:
text
事务提交
不会回滚。
原因:
Spring判断:
text
方法是否抛出异常
这里:
java
catch
已经吞掉异常。
Spring感知不到异常。
解决:
继续抛出:
java
catch (Exception e) {
throw e;
}
或者:
java
TransactionAspectSupport
.currentTransactionStatus()
.setRollbackOnly();
八、事务失效场景四:受检异常
默认情况下:
java
@Transactional
只会回滚:
java
RuntimeException
Error
例如:
java
throw new RuntimeException();
事务回滚。
但是:
java
throw new IOException();
事务不会回滚。
解决方案:
java
@Transactional(
rollbackFor = Exception.class
)
推荐统一写法:
java
@Transactional(
rollbackFor = Exception.class
)
九、事务失效场景五:多线程事务失效
这是实际项目最容易踩坑的问题。
示例:
java
@Transactional
public void save() {
userMapper.insert(user);
new Thread(() -> {
orderMapper.insert(order);
}).start();
int a = 1 / 0;
}
开发者期望:
text
异常发生
↓
全部回滚
实际结果:
text
user回滚
order提交
为什么?
因为:
text
事务基于ThreadLocal
主线程:
text
Thread-A
↓
事务A
新线程:
text
Thread-B
↓
没有事务
因此:
java
orderMapper.insert()
直接提交。
十、事务失效场景六:手动new对象
错误示例:
java
UserService service =
new UserService();
调用:
java
service.save();
原因:
text
Spring没有管理该对象
没有代理对象。
事务无法生效。
正确写法:
java
@Autowired
private UserService userService;
十一、事务失效场景七:数据库不支持事务
例如:
MySQL:
text
MyISAM
引擎。
查看:
sql
SHOW TABLE STATUS;
如果:
text
Engine=MyISAM
事务无效。
推荐:
text
InnoDB
十二、多线程事务失效深度解析
这是面试中的高频问题。
代码:
java
@Transactional
public void saveOrder() {
}
Spring执行:
text
ThreadLocal
↓
Connection
↓
Transaction
绑定。
如果开启新线程:
java
CompletableFuture.runAsync(...)
或者:
java
new Thread(...)
线程切换后:
text
ThreadLocal丢失
新线程获取不到:
text
事务上下文
最终:
text
事务失效
十三、多线程事务解决方案
方案一:避免事务中开启线程
推荐:
text
事务负责事务
线程负责线程
解耦。
方案二:消息队列
例如:
text
RabbitMQ
Kafka
RocketMQ
主事务提交后:
text
发送消息
异步线程消费。
方案三:子线程独立事务
例如:
java
@Transactional(
propagation =
Propagation.REQUIRES_NEW
)
每个线程独立事务。
方案四:编程式事务
java
TransactionTemplate
手动控制事务。
十四、事务传播行为
常见传播行为:
| 传播级别 | 说明 |
|---|---|
| REQUIRED | 默认,有事务加入,无事务创建 |
| REQUIRES_NEW | 创建新事务 |
| SUPPORTS | 支持事务 |
| NOT_SUPPORTED | 不支持事务 |
| MANDATORY | 必须存在事务 |
| NEVER | 必须不存在事务 |
| NESTED | 嵌套事务 |
最常用:
java
REQUIRED
和:
java
REQUIRES_NEW
十五、面试高频问题
面试题1
Spring事务实现原理?
答案:
text
AOP + 数据库事务
面试题2
为什么同类调用事务失效?
答案:
text
绕过代理对象
面试题3
为什么try-catch导致事务失效?
答案:
text
异常被吞掉
Spring感知不到异常
面试题4
为什么多线程事务失效?
答案:
text
事务基于ThreadLocal
线程切换后
获取不到事务上下文
面试题5
@Transactional默认回滚哪些异常?
答案:
text
RuntimeException
Error
面试题6
如何让所有异常回滚?
答案:
java
@Transactional(
rollbackFor = Exception.class
)
十六、开发最佳实践
统一写法:
java
@Transactional(
rollbackFor = Exception.class
)
避免:
java
new Thread()
出现在事务中。
避免:
java
this.xxx()
调用事务方法。
避免:
java
try-catch
吞掉异常。
优先使用:
text
消息队列
异步任务
事件驱动
处理事务外逻辑。
总结
Spring事务本质:
text
AOP + ThreadLocal + 数据库事务
事务失效高频场景:
- 非public方法
- 同类内部调用
- 异常被捕获
- 抛出受检异常
- 多线程调用
- 手动new对象
- 数据库不支持事务
其中面试最常问的是:
text
同类调用事务失效
和:
text
多线程事务失效
牢记一句话:
Spring事务与当前线程绑定,事务上下文存储在ThreadLocal中。线程切换后无法共享事务上下文,因此多线程场景下事务天然失效。