1. Spring事务失效的典型场景剖析
从事Java开发这些年,我见过太多因为事务配置不当导致的线上事故。上周团队里有个新人写的订单服务就出现了金额扣减成功但库存未更新的情况,排查后发现是事务注解放错了位置。今天我们就来系统梳理Spring事务失效的12种常见场景,这些都是我用真金白银的线上事故换来的经验。
Spring事务本质上是通过AOP代理实现的,理解这点很重要。当你在方法上添加@Transactional注解时,Spring会在运行时创建一个代理对象来管理事务边界。但有些情况下这个机制会被破坏,导致事务根本不生效。下面这些坑,建议每个Java开发者都牢记在心。
2. 事务失效的12种典型案例
2.1 注解放在非public方法上
java复制@Service
public class OrderService {
@Transactional
private void updateStock(Long productId) { // 不会生效!
// 库存更新逻辑
}
}
这是新手最容易犯的错误。Spring默认使用基于代理的AOP,而private/protected方法无法被代理。解决方案要么改为public方法,要么改用AspectJ的编译时织入方式。
关键点:Spring事务代理只能拦截public方法,这是由Spring AOP的实现机制决定的
2.2 自调用问题
java复制public void createOrder(OrderDTO dto) {
validate(dto);
updateStock(dto.getProductId()); // 自调用事务不生效
}
@Transactional
public void updateStock(Long productId) {
// 库存操作
}
当类内部方法互相调用时,走的是this指针而不是代理对象。解决方法有:
- 将事务方法拆分到另一个Service
- 通过ApplicationContext获取代理对象
- 使用AspectJ模式
2.3 异常类型不匹配
java复制@Transactional
public void process() throws Exception {
try {
// 业务代码
} catch (BusinessException e) {
throw new Exception("转换异常"); // 事务不会回滚!
}
}
Spring默认只对RuntimeException和Error回滚。建议:
- 抛出原生的RuntimeException
- 或设置@Transactional(rollbackFor=Exception.class)
2.4 多数据源未指定
java复制@Transactional // 默认用primary数据源
public void multiDBOperation() {
jdbcTemplateA.update(...); // 操作数据源A
jdbcTemplateB.update(...); // 操作数据源B
}
在多数据源环境下,必须明确指定事务管理器:
java复制@Transactional("orderTransactionManager")
2.5 传播行为设置不当
java复制@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void batchProcess() {
// 即使被其他事务方法调用,也不会加入事务
}
特别注意SUPPORTS/NOT_SUPPORTED/NEVER这些非强制事务的传播行为。
3. 事务失效的深度排查技巧
3.1 日志分析三板斧
- 开启debug日志:
properties复制logging.level.org.springframework.transaction=debug
logging.level.org.springframework.jdbc=debug
- 观察事务启停日志:
code复制Creating new transaction...
Initiating transaction rollback...
- 检查SQL执行日志的时间戳,确认是否在同一事务内
3.2 动态代理检查工具
java复制// 在方法内打印this.getClass()
System.out.println("当前类:" + getClass().getName());
// 正常应该输出:com.sun.proxy.$ProxyXX
// 如果是原始类名说明代理失效
3.3 事务隔离级别验证
java复制@Transactional(isolation = Isolation.READ_COMMITTED)
public void checkIsolation() {
// 执行SELECT @@tx_isolation; 验证隔离级别
}
4. 最佳实践方案
4.1 统一事务配置模板
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
timeout = 30,
rollbackFor = Throwable.class
)
public @interface AppTransactional {
}
4.2 事务拆分原则
- 读操作:@Transactional(readOnly=true)
- 写操作:单独事务方法
- 批量操作:考虑编程式事务
4.3 性能优化建议
- 避免长事务:设置合理timeout
- 只读事务标记readOnly
- 非必要不开启新事务
5. 高频问题排查实录
5.1 MySQL引擎问题
sql复制-- 检查表引擎
SHOW TABLE STATUS LIKE 'order_table';
-- 只有InnoDB支持事务
ALTER TABLE order_table ENGINE=InnoDB;
5.2 连接池配置
yaml复制spring:
datasource:
hikari:
auto-commit: false # 必须关闭自动提交
5.3 特殊框架整合
与MyBatis整合时注意:
java复制@MapperScan(basePackages = "com.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
@EnableTransactionManagement(proxyTargetClass = true) // 需要CGLIB代理
6. 事务监控方案
推荐使用Prometheus监控:
java复制@Bean
public TransactionMetrics transactionMetrics(PlatformTransactionManager tm) {
return new TransactionMetrics(tm);
}
关键指标:
- 事务成功率
- 平均持续时间
- 回滚率
7. 终极解决方案
当所有检查都通过但事务还是不生效时,可以尝试:
- 清理项目重新编译
- 检查Spring版本兼容性
- 升级JDK版本(某些版本有动态代理bug)
最后分享一个我压箱底的调试技巧:在事务方法开始和结束时插入Thread.sleep(5000),然后观察数据库中间状态,可以直观验证事务边界。