摘要:从一次"明明加了@Transactional却没回滚"的诡异bug出发,深度剖析Spring事务的5大致命细节。通过事务传播行为的7种场景、异常类型导致的回滚失效、以及事务失效的8种原因,揭秘为什么private方法事务不生效、为什么try-catch会吞掉事务、以及自调用为什么绕过AOP代理。配合时序图展示事务代理流程,手写简易版事务管理器,给出事务使用的最佳实践。
💥 翻车现场
周三上午,测试同学报了一个bug。
测试同学:@哈吉米 转账功能有问题!扣款失败了,但钱还是被扣了!
哈吉米:不可能啊,我加了@Transactional,出错应该回滚的!
查看代码:
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Transactional
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
// 1. 扣减转出方余额
accountMapper.decreaseBalance(fromUserId, amount);
// 2. 增加转入方余额
accountMapper.increaseBalance(toUserId, amount);
// 3. 调用外部接口(可能失败)
try {
externalService.notify(fromUserId, toUserId, amount);
} catch (Exception e) {
log.error("通知失败", e);
// 吞掉了异常,事务不会回滚 ❌
}
}
}
测试场景:
操作:A给B转账100元
结果:
- A余额:1000 → 900(扣了)
- B余额:500 → 500(没加)
- 外部接口调用失败(抛异常)
- 但异常被catch了,事务没回滚 ❌
问题:A的钱扣了,B没收到,钱丢了!
哈吉米:"卧槽,try-catch会导致事务不回滚?"
南北绿豆和阿西噶阿西来了。
南北绿豆:"这是Spring事务的第一个坑:异常被catch后,事务不会回滚!"
哈吉米:"那怎么办?"
阿西噶阿西:"有很多细节要注意,我给你讲讲Spring事务的5大致命细节。"
🕳️ 致命细节1:异常被catch后不回滚
问题原因
南北绿豆:"Spring事务的回滚机制是:捕获未处理的异常,触发回滚。"
// Spring事务的伪代码
try {
// 开启事务
beginTransaction();
// 执行业务方法
transfer(...);
// 提交事务
commitTransaction();
} catch (Exception e) {
// 捕获到异常,回滚
rollbackTransaction();
throw e;
}
如果你catch了异常:
@Transactional
public void transfer(...) {
try {
decreaseBalance(...);
increaseBalance(...);
externalService.notify(...); // 抛异常
} catch (Exception e) {
log.error("异常", e); // 异常被吞了
// Spring看不到异常,认为执行成功,提交事务 ❌
}
}
解决方案
方案1:不要catch,让异常抛出
@Transactional
public void transfer(...) {
decreaseBalance(...);
increaseBalance(...);
externalService.notify(...); // 让异常抛出
}
方案2:catch后手动回滚
@Transactional
public void transfer(...) {
try {
decreaseBalance(...);
increaseBalance(...);
externalService.notify(...);
} catch (Exception e) {
log.error("异常", e);
// 手动标记回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw e; // 必须重新抛出
}
}
方案3:拆分事务(推荐)
@Transactional
public void transfer(...) {
// 核心事务逻辑
decreaseBalance(...);
increaseBalance(...);
}
// 外部调用(事务外)
public void transferAndNotify(...) {
transfer(...); // 事务方法
// 事务提交后,再调用外部接口
try {
externalService.notify(...);
} catch (Exception e) {
log.error("通知失败", e);
// 这里catch不影响事务(事务已提交)
}
}
🕳️ 致命细节2:只有RuntimeException才回滚
默认行为
@Transactional
public void transfer(...) {
decreaseBalance(...);
throw new Exception("checked异常"); // checked异常,不会回滚 ❌
}
@Transactional
public void transfer(...) {
decreaseBalance(...);
throw new RuntimeException("运行时异常"); // RuntimeException,会回滚 ✅
}
Spring事务的默认规则:
| 异常类型 | 是否回滚 |
|---|---|
| RuntimeException(运行时异常) | ✅ 回滚 |
| Error | ✅ 回滚 |
| Exception(checked异常) | ❌ 不回滚 |
为什么这样设计?
南北绿豆:"因为checked异常通常是可预期的,如文件不存在、网络超时,不一定需要回滚。"
// 示例:文件上传
@Transactional
public void uploadFile(File file) throws IOException {
// 保存文件记录
fileMapper.insert(fileRecord);
// 上传文件(可能抛IOException)
ossService.upload(file); // 抛IOException(checked异常)
// 如果上传失败,是否要回滚文件记录?
// 不一定,可能只需要标记"上传失败"
}
解决方案:指定回滚的异常
// 方案1:回滚所有异常
@Transactional(rollbackFor = Exception.class)
public void transfer(...) throws Exception {
decreaseBalance(...);
externalService.notify(...); // 抛checked异常也会回滚
}
// 方案2:指定特定异常回滚
@Transactional(rollbackFor = {BusinessException.class, IOException.class})
public void upload(...) {
// ...
}
// 方案3:指定不回滚的异常
@Transactional(noRollbackFor = {FileNotFoundException.class})
public void process(...) {
// FileNotFoundException不回滚,其他异常回滚
}
最佳实践:
// 推荐写法
@Transactional(rollbackFor = Exception.class)
public void businessMethod() {
// ...
}
🕳️ 致命细节3:事务传播行为的7种场景
什么是事务传播?
@Transactional
public void methodA() {
// 事务A
methodB(); // methodB也有@Transactional,怎么办?
}
@Transactional
public void methodB() {
// 用事务A?还是新建事务B?
}
7种传播行为
| 传播行为 | 含义 | 场景 |
|---|---|---|
| REQUIRED(默认) | 有事务就用,没有就新建 | ⭐⭐⭐⭐⭐ 最常用 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 | 独立事务(日志记录) |
| SUPPORTS | 有事务就用,没有就不用 | 查询方法 |
| NOT_SUPPORTED | 总是非事务执行,挂起当前事务 | 非事务方法 |
| MANDATORY | 必须在事务中,否则抛异常 | 强制事务 |
| NEVER | 不能在事务中,否则抛异常 | 强制非事务 |
| NESTED | 嵌套事务(子事务) | 部分回滚 |
场景1:REQUIRED(默认)
@Transactional
public void methodA() {
// 事务A
methodB();
}
@Transactional(propagation = Propagation.REQUIRED) // 默认
public void methodB() {
// 用事务A(不新建)
}
// 结果:methodA和methodB在同一个事务中
// methodB抛异常 → methodA也回滚
场景2:REQUIRES_NEW(新建事务)
@Transactional
public void methodA() {
// 事务A
insertOrder(); // 插入订单
methodB(); // 新建事务B
// 如果这里抛异常,订单回滚,但日志不回滚
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 新建事务B(独立于事务A)
insertLog(); // 插入日志
}
// 结果:
// - methodB新建事务B,独立提交
// - 即使methodA回滚,methodB的日志仍然保存
时序图:
sequenceDiagram
participant MethodA as methodA(事务A)
participant TxnA as 事务A
participant MethodB as methodB(REQUIRES_NEW)
participant TxnB as 事务B
MethodA->>TxnA: 1. 开启事务A
MethodA->>MethodA: 2. insertOrder()
MethodA->>MethodB: 3. 调用methodB()
MethodB->>TxnA: 4. 挂起事务A
MethodB->>TxnB: 5. 新建事务B
MethodB->>MethodB: 6. insertLog()
MethodB->>TxnB: 7. 提交事务B ✅
MethodB->>TxnA: 8. 恢复事务A
MethodA->>MethodA: 9. 抛异常
MethodA->>TxnA: 10. 回滚事务A ❌
Note over TxnB: 事务B已提交,日志保存 ✅
Note over TxnA: 事务A回滚,订单删除 ❌
应用场景:记录操作日志(无论业务成功失败,日志都要记录)
场景3:NESTED(嵌套事务)
@Transactional
public void methodA() {
insertOrder();
try {
methodB(); // 嵌套事务
} catch (Exception e) {
// methodB回滚,但methodA可以继续
}
insertLog();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
insertDetail();
throw new RuntimeException(); // 抛异常
}
// 结果:
// - methodB回滚(明细不插入)
// - methodA不回滚(订单和日志插入)
NESTED vs REQUIRES_NEW:
| 特性 | NESTED | REQUIRES_NEW |
|---|---|---|
| 是否新建事务 | 否(用Savepoint) | 是 |
| 子事务回滚 | 不影响外层 | 不影响外层 |
| 外层回滚 | 子事务也回滚 | 子事务不回滚(已提交) |
🕳️ 致命细节4:事务失效的8种原因
原因1:方法不是public
@Service
public class UserService {
// ❌ private方法,事务不生效
@Transactional
private void updateUser(User user) {
userMapper.updateById(user);
}
// ✅ public方法,事务生效
@Transactional
public void updateUserPublic(User user) {
userMapper.updateById(user);
}
}
原因:Spring事务基于AOP代理,只能代理public方法。
原因2:自调用(同一个类内部调用)
@Service
public class UserService {
public void methodA() {
// 自调用methodB
this.methodB(); // 不走代理,事务不生效 ❌
}
@Transactional
public void methodB() {
userMapper.updateById(user);
}
}
原理图:
graph LR
A[Controller] --> B[UserService代理对象]
B --> C{调用methodA}
C --> D[methodA 不走代理]
D --> E[this.methodB 直接调用]
E --> F[methodB 不走代理]
G[正确调用] --> H[UserService代理对象]
H --> I[methodB 走代理]
I --> J[开启事务]
J --> K[执行方法]
K --> L[提交/回滚]
style F fill:#FFB6C1
style L fill:#90EE90
解决方案:
// 方案1:注入自己(走代理)
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己
public void methodA() {
self.methodB(); // 走代理,事务生效 ✅
}
@Transactional
public void methodB() {
userMapper.updateById(user);
}
}
// 方案2:通过AopContext获取代理对象
public void methodA() {
UserService proxy = (UserService) AopContext.currentProxy();
proxy.methodB(); // 走代理
}
// 方案3:拆成两个Service
@Service
public class UserServiceA {
@Autowired
private UserServiceB serviceB;
public void methodA() {
serviceB.methodB(); // 走代理 ✅
}
}
@Service
public class UserServiceB {
@Transactional
public void methodB() {
userMapper.updateById(user);
}
}
原因3:数据库引擎不支持事务
-- MyISAM不支持事务
CREATE TABLE user (
id BIGINT PRIMARY KEY,
...
) ENGINE=MyISAM; -- ❌ 不支持事务
-- InnoDB支持事务
CREATE TABLE user (
id BIGINT PRIMARY KEY,
...
) ENGINE=InnoDB; -- ✅ 支持事务
原因4:没有被Spring管理
// ❌ 错误(没有@Service)
public class UserService {
@Transactional
public void updateUser(User user) {
// 不是Spring Bean,事务不生效
}
}
// ✅ 正确
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
// ...
}
}
原因5-8(快速总结)
| 原因 | 说明 |
|---|---|
| 5. 方法被final修饰 | 无法被代理 |
| 6. 异步方法@Async | 新线程,事务传播失效 |
| 7. 传播行为设置错误 | 如NEVER、NOT_SUPPORTED |
| 8. 多数据源未正确配置 | 事务管理器配置错误 |
🎯 Spring事务的内部原理
事务代理的流程
阿西噶阿西:"Spring事务是基于AOP动态代理实现的。"
// 原始Service
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
userMapper.updateById(user);
}
}
// Spring生成的代理类(伪代码)
public class UserService$$EnhancerBySpringCGLIB {
private UserService target; // 原始对象
public void updateUser(User user) {
TransactionInfo txInfo = null;
try {
// 1. 开启事务
txInfo = createTransactionIfNecessary();
// 2. 调用原始方法
target.updateUser(user);
// 3. 提交事务
commitTransactionAfterReturning(txInfo);
} catch (Throwable ex) {
// 4. 回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// 5. 清理事务信息
cleanupTransactionInfo(txInfo);
}
}
}
完整时序图
sequenceDiagram
participant Controller
participant Proxy as Service代理对象
participant TxManager as 事务管理器
participant Target as Service原始对象
participant DB as 数据库
Controller->>Proxy: 1. 调用updateUser()
Proxy->>TxManager: 2. 开启事务
TxManager->>DB: 3. BEGIN TRANSACTION
Proxy->>Target: 4. 调用原始方法
Target->>DB: 5. UPDATE user ...
alt 方法执行成功
Target->>Proxy: 6. 返回
Proxy->>TxManager: 7. 提交事务
TxManager->>DB: 8. COMMIT
Proxy->>Controller: 9. 返回结果
else 方法抛异常
Target->>Proxy: 6. 抛异常
Proxy->>TxManager: 7. 回滚事务
TxManager->>DB: 8. ROLLBACK
Proxy->>Controller: 9. 抛异常
end
哈吉米:"所以事务是由代理对象控制的,自调用绕过了代理,事务就失效了!"
🛠️ 使用ThreadLocal管理事务
事务信息的存储
// Spring事务管理器(简化)
public class TransactionManager {
// 用ThreadLocal存储事务信息
private static final ThreadLocal<TransactionInfo> TX_INFO = new ThreadLocal<>();
public void begin() {
// 开启事务
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 存储到ThreadLocal
TransactionInfo info = new TransactionInfo(conn);
TX_INFO.set(info);
}
public void commit() {
TransactionInfo info = TX_INFO.get();
if (info != null) {
info.getConnection().commit();
}
}
public void rollback() {
TransactionInfo info = TX_INFO.get();
if (info != null) {
info.getConnection().rollback();
}
}
public void cleanup() {
TransactionInfo info = TX_INFO.get();
if (info != null) {
info.getConnection().close();
TX_INFO.remove(); // 清理ThreadLocal
}
}
}
南北绿豆:"事务信息存在ThreadLocal中,保证同一个线程内的多个方法共享同一个事务。"
🎓 面试标准答案
题目:@Transactional什么时候会失效?
答案:
8种失效场景:
- 方法不是public → AOP只能代理public方法
- 自调用 → 绕过代理对象
- 异常被catch → Spring看不到异常
- 异常类型不匹配 → checked异常默认不回滚
- 数据库引擎不支持 → MyISAM不支持事务
- 没有被Spring管理 → 不是Bean
- 方法被final修饰 → 无法被代理
- 多数据源配置错误 → 事务管理器不对
解决方案:
- 方法改public
- 注入自己或拆Service
- 不要catch或手动setRollbackOnly
- 设置rollbackFor = Exception.class
- 用InnoDB引擎
- 加@Service注解
题目:事务传播行为有哪些?
答案(见7种传播行为表格)
常用的3种:
- REQUIRED:默认,有事务就用
- REQUIRES_NEW:新建独立事务
- NESTED:嵌套事务(Savepoint)
🎉 结束语
晚上10点,哈吉米把所有事务的坑都填完了。
哈吉米:"原来@Transactional有这么多坑!try-catch、自调用、方法访问权限……"
南北绿豆:"对,事务看起来简单,但细节很多。"
阿西噶阿西:"记住:public方法、不要自调用、不要catch异常、设置rollbackFor。"
哈吉米:"还有事务传播行为,REQUIRED和REQUIRES_NEW的区别要搞清楚。"
南北绿豆:"对,理解了原理和代理机制,就知道为什么会失效了!"
记忆口诀:
事务基于AOP代理,public方法才生效
自调用绕过代理,异常catch不回滚
RuntimeException默回滚,checked异常要指定
传播行为七种姿势,REQUIRED最常用
用完记得cleanup,ThreadLocal别泄漏
希望这篇文章能帮你避开Spring事务的所有坑!记住:理解代理机制,才能理解事务的行为!💪