从事Java开发这些年,Spring事务管理就像个熟悉的陌生人——看似简单,实则暗藏玄机。记得刚工作时,线上系统突然出现数据不一致,排查半天才发现是事务没生效。今天我就把这些年踩过的坑和解决方案整理成这份实战指南,涵盖8大类高频失效场景及其背后的原理。
Spring事务本质是通过AOP代理实现的。当我们使用@Transactional注解时,Spring会创建一个代理对象包裹目标方法。这个代理对象负责在方法执行前开启事务,执行后提交或回滚。关键实现类包括:
java复制// 典型的事务代理逻辑伪代码
public Object invoke(MethodInvocation invocation) {
TransactionStatus status = transactionManager.getTransaction(defination);
try {
Object result = invocation.proceed();
transactionManager.commit(status);
return result;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
| 传播行为类型 | 说明 | 典型应用场景 |
|---|---|---|
| REQUIRED | 当前有事务则加入,没有则新建 | 普通增删改操作 |
| REQUIRES_NEW | 新建独立事务,挂起当前事务 | 日志记录等独立操作 |
| NESTED | 嵌套事务,可部分回滚 | 复杂业务子流程 |
| SUPPORTS | 有事务则加入,没有则非事务运行 | 查询操作 |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 | 发送消息等非核心操作 |
| MANDATORY | 必须在事务中调用,否则抛异常 | 强制事务场景 |
| NEVER | 必须在非事务环境调用 | 特殊校验场景 |
这是新手最容易踩的坑。当类内部方法A调用带@Transactional的方法B时,事务不会生效:
java复制@Service
public class OrderService {
public void createOrder(Order order) {
// 内部调用导致事务失效
this.saveOrderLog(order);
}
@Transactional
public void saveOrderLog(Order order) {
// 日志记录操作
}
}
解决方案:
java复制// 方案3示例
((OrderService)AopContext.currentProxy()).saveOrderLog(order);
Spring默认只对RuntimeException和Error进行回滚。如果捕获了异常却不抛出:
java复制@Transactional
public void updateUser(User user) {
try {
userDao.update(user);
// 可能抛出SQLException
} catch (Exception e) {
log.error("更新失败", e); // 吞掉异常导致事务不会回滚
}
}
正确做法:
java复制@Transactional(rollbackFor = Exception.class) // 指定所有异常都回滚
public void updateUser(User user) throws Exception {
userDao.update(user);
}
使用MyISAM引擎的表不会支持事务。检查建表语句:
sql复制CREATE TABLE orders (
id BIGINT PRIMARY KEY
) ENGINE=MyISAM; -- 应改为InnoDB
检查方法:
sql复制SHOW TABLE STATUS LIKE 'orders';
REQUIRES_NEW使用不当会导致外层事务失效:
java复制@Transactional
public void outerMethod() {
innerMethod(); // 如果内层抛出异常
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
// 独立事务提交后,外层事务无法回滚此操作
}
最佳实践:
非public方法上的@Transactional会失效:
java复制@Transactional
private void saveData() { // 事务不生效
// ...
}
原理:
Spring基于代理的实现要求目标方法必须是public的,因为CGLIB无法代理private方法。
当配置多个数据源时,必须明确指定事务管理器:
java复制@Transactional("orderTransactionManager") // 指定事务管理器bean名称
public void saveOrder(Order order) {
// 使用orderDataSource的操作
}
@Async和@Transactional混用会导致事务不传播到异步线程:
java复制@Async
@Transactional // 事务不会生效
public void asyncProcess() {
// 异步操作
}
解决方案:
final/static方法上的事务注解无效:
java复制@Transactional
public final void finalMethod() { // 事务失效
// ...
}
在application.properties中添加:
properties复制logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
java复制@Test
public void testTransactionRollback() {
try {
service.methodThatShouldRollback();
fail("Expected exception");
} catch (Exception e) {
// 验证数据是否回滚
assertFalse(dataExistsInDb());
}
}
java复制public static void printTransactionStatus() {
System.out.println("当前事务状态: " +
TransactionSynchronizationManager.isActualTransactionActive());
System.out.println("当前隔离级别: " +
TransactionSynchronizationManager.getCurrentTransactionIsolationLevel());
}
典型的大事务反模式:
java复制@Transactional
public void batchProcess(List<Data> dataList) {
for (Data data : dataList) { // 循环处理
processSingle(data); // 耗时操作
}
}
优化方案:
| 方案 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| 2PC | 两阶段提交 | 强一致性要求 | 性能差,实现复杂 |
| TCC | Try-Confirm-Cancel | 高并发场景 | 业务侵入性强 |
| SAGA | 长事务拆分 | 跨服务业务流程 | 需实现补偿机制 |
| 本地消息表 | 可靠消息 | 最终一致性 | 需消息中间件 |
特别注意flush和事务的关系:
java复制@Transactional
public void updateWithFlush() {
entityManager.persist(entity);
entityManager.flush(); // 强制同步到数据库但事务未提交
// 此时其他事务仍看不到此变更
}
java复制@Transactional(timeout = 5) // 单位:秒
public void timeCriticalOperation() {
// ...
}
java复制@Transactional(readOnly = true)
public List<Order> queryOrders(Date date) {
// 查询操作
}
优化效果:
可通过Micrometer暴露这些指标到Prometheus。
| 现象 | 可能原因 | 快速检查点 |
|---|---|---|
| 事务不生效 | 同类调用/非public方法 | 检查调用栈/方法修饰符 |
| 异常不回滚 | 错误异常类型/捕获异常 | 检查rollbackFor配置 |
| 部分更新生效 | 传播行为配置不当 | 检查REQUIRES_NEW使用 |
| 性能低下 | 大事务/缺少只读标记 | 检查方法执行时间 |
| 连接泄漏 | 未正确关闭资源 | 检查try-with-resources |
事务注解要尽量放在具体方法上,而不是类级别。类级别注解容易被误用
避免在事务方法中进行RPC调用,这会导致事务时间不可控。必要时考虑异步补偿
测试阶段开启事务日志,我通常在开发环境配置如下日志级别:
properties复制logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=TRACE
使用TransactionTemplate处理复杂场景:
java复制new TransactionTemplate(transactionManager).execute(status -> {
// 复杂事务逻辑
return result;
});
定期检查事务配置,特别是当引入新数据源或ORM框架时。曾经因为MyBatis配置错误导致事务失效,排查了整整一天