theme: cyanosis
深入解析 @Transactional
——Spring 事务管理的核心
@Transactional
是什么?
在开发 Spring Boot 项目的时候,我们往往会遇到这样的场景:一个方法里执行了多步数据库操作,但中间某一步出错了,导致数据出现了“半成功半失败”的情况。这样的数据不一致问题,可能会带来严重的业务风险。而 @Transactional
就是为了解决这个问题而生的。
它的核心作用就是 要么所有操作都成功,要么全部作废,保证数据库的完整性。但很多人用 @Transactional
时,总会遇到“事务没生效”、“回滚失败”、“操作部分成功”等问题,这背后其实有很多坑需要避开。今天我们就来深入拆解 @Transactional
的工作机制、常见陷阱以及最佳实践,帮你用好 Spring 的事务管理能力。
@Transactional
的基本用法
先来看一个最简单的示例,假设我们有一个删除部门的方法:
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Transactional
public void deleteDept(Long id) {
deptMapper.delete(id);
int error = 1 / 0; // 故意制造异常
}
}
这个方法的逻辑很简单:删除部门后,我们故意制造了一个除零错误,看看事务是否会回滚。如果 @Transactional
正常生效,那么删除操作就不会被提交。但如果事务没生效,那就尴尬了,部门被删掉了,但异常依然抛出了。
Spring 事务默认的回滚规则
有时候,你可能会发现明明 @Transactional
加上了,但事务就是没回滚。Spring 默认的规则是:只有未捕获的 RuntimeException
(运行时异常)或 Error
才会触发回滚,而普通的 Exception
(检查异常)不会触发回滚。
举个例子:
@Transactional
public void deleteDept(Long id) throws IOException {
deptMapper.delete(id);
throw new IOException("不会回滚!");
}
这里抛出了 IOException
,但数据库的删除操作依然提交了。这是因为 IOException
是检查异常(Checked Exception),Spring 默认不会回滚。如果你希望无论什么异常都触发回滚,需要这样写:
@Transactional(rollbackFor = Exception.class)
public void deleteDept(Long id) throws IOException {
deptMapper.delete(id);
throw new IOException("现在会回滚了!");
}
所以,如果你希望所有异常都能回滚,最好加上 rollbackFor = Exception.class
,避免出现“事务看起来生效了,但并没有真正回滚”的情况。
try-catch
导致事务失效
你可能会写这样的代码:
@Transactional
public void deleteDept(Long id) {
try {
deptMapper.delete(id);
int x = 1 / 0; // 触发异常
} catch (Exception e) {
System.out.println("发生异常,但事务未回滚");
}
}
看上去是个挺合理的异常处理,但问题来了:事务不会回滚!
为什么?因为异常被 catch
了,Spring 根本感知不到异常的发生,认为你的方法执行得好好的,于是就把事务提交了。
正确的做法是要么 让异常抛出去,要么 手动抛出一个新的 RuntimeException
:
@Transactional
public void deleteDept(Long id) {
try {
deptMapper.delete(id);
int x = 1 / 0;
} catch (Exception e) {
throw new RuntimeException("手动抛出异常,确保事务回滚", e);
}
}
或者直接这样写,让异常自然传播:
@Transactional
public void deleteDept(Long id) throws Exception {
deptMapper.delete(id);
int x = 1 / 0; // 事务会回滚
}
总结一下:Spring 只有在方法抛出异常时,才会触发回滚。如果你在 catch
里吞掉了异常,那事务也就不会回滚了。
事务为什么有时候会失效?
除了 try-catch
,还有一些常见情况会让 @Transactional
失效,看看你是否踩过这些坑。
1. 方法不是 public
@Transactional
只会作用于 public 方法,如果你加在 private
或 protected
方法上,事务不会生效:
@Transactional
private void deleteDept(Long id) {
} // 事务不会生效!
Spring 事务是通过 代理机制 实现的,而 JDK 动态代理只能代理 public
方法,所以其他访问级别的方法都不行。正确写法是:
@Transactional
public void deleteDept(Long id) {
}
2. 同一类里,方法互相调用
看下面的代码:
@Service
public class DeptServiceImpl {
@Autowired
private DeptMapper deptMapper;
@Transactional
public void deleteDept(Long id) {
this.deleteEmp(id); // 事务不会生效!
}
@Transactional
public void deleteEmp(Long id) {
empMapper.delByDeptId(id);
}
}
这里 deleteDept
方法调用了 deleteEmp
,但 deleteEmp
上的 @Transactional
不会生效!原因是:Spring 的事务是基于代理的,this.deleteEmp(id)
直接调用了本类的方法,没有经过 Spring 代理,所以事务不会生效。
正确的做法是 通过 Spring 管理的 Bean 调用:
@Service
public class DeptServiceImpl {
@Autowired
private DeptServiceImpl self;
@Transactional
public void deleteDept(Long id) {
self.deleteEmp(id); // 事务生效!
}
@Transactional
public void deleteEmp(Long id) {
empMapper.delByDeptId(id);
}
}
或者使用 ApplicationContext
获取代理对象,再调用方法。
3. 数据库引擎不支持事务
如果你用的 MySQL 表引擎是 MyISAM,事务是不可能生效的,因为 MyISAM 根本不支持事务!要确保你的表是 InnoDB:
SHOW TABLE STATUS WHERE Name = 'dept';
ALTER TABLE dept ENGINE = InnoDB;
@Transactional
的最佳实践
- 确保事务方法是
public
,否则事务不会生效。 - 避免同一类内部调用
@Transactional
方法,可以使用self.xxx()
代理调用。 - 异常要让 Spring 感知到,不要
try-catch
后直接吞掉。 - 检查异常默认不会回滚,如果需要回滚,使用
rollbackFor = Exception.class
。 - 数据库表必须支持事务,MyISAM 不支持事务,建议用 InnoDB。
总结
Spring 事务管理的核心思想是 原子性,让数据库操作要么全成功,要么全失败。但如果用 @Transactional
时不小心踩了坑,可能会导致事务失效,影响数据一致性。希望这篇文章能帮你理解 @Transactional
的运行原理,让你的 Spring Boot 开发更加稳定可靠!