1. Spring事务失效的典型场景剖析
从事Java开发这些年,我见过太多因为事务配置不当导致的线上事故。上周团队里一个新人写的订单服务就出现了金额扣减成功但库存未更新的严重问题,排查后发现是事务失效导致的。今天我就结合自己踩过的坑,系统梳理Spring事务失效的12种典型场景,每个场景都会给出可复现的测试用例。
重要提示:所有示例基于Spring Boot 2.7 + JDK 11环境,使用@Transactional注解的默认隔离级别(ISOLATION_DEFAULT)和传播行为(PROPAGATION_REQUIRED)
1.1 自调用问题
这是最常见的失效场景之一。当类内部方法调用同类中带有@Transactional注解的方法时,事务不会生效:
@Service public class OrderService { public void createOrder(OrderDTO dto) { // 自调用导致事务失效 this.deductStock(dto); } @Transactional public void deductStock(OrderDTO dto) { // 库存扣减操作 } }失效原因:Spring事务基于AOP实现,自调用会绕过代理对象。解决方案有三种:
- 将方法拆分到不同类
- 通过ApplicationContext获取代理对象
- 使用AspectJ模式替代动态代理
1.2 异常类型不匹配
@Transactional public void processPayment() { try { paymentDao.updateStatus(); // 这里抛出IOException } catch (Exception e) { // 吞掉异常 } }关键点:
- 默认只对RuntimeException和Error回滚
- 检查异常需要明确指定rollbackFor
- 不要catch后不处理异常
1.3 数据库引擎不支持
曾遇到过使用MySQL的MyISAM引擎导致事务失效的案例:
CREATE TABLE account ( id BIGINT PRIMARY KEY, balance DECIMAL(10,2) ) ENGINE=MyISAM; -- 必须改为InnoDB2. 传播行为配置不当引发的陷阱
2.1 REQUIRES_NEW误用
@Transactional public void parentMethod() { childService.childMethod(); // 传播行为配置为REQUIRES_NEW throw new RuntimeException(); // 父事务回滚不影响子事务 }实测现象:
- parentMethod回滚
- childMethod提交
- 造成数据不一致
2.2 NESTED的注意事项
@Transactional(propagation = Propagation.NESTED) public void nestedOperation() { // 需要数据库支持保存点(SQL Server不支持) }3. 事务隔离级别的深坑
3.1 幻读问题重现
@Transactional(isolation = Isolation.READ_COMMITTED) public void transfer() { // 第一次查询余额 BigDecimal balance1 = accountDao.getBalance(); // 此时另一个事务插入新记录 accountDao.insertNewRecord(); // 第二次查询结果不同 BigDecimal balance2 = accountDao.getBalance(); }解决方案:
- 使用SERIALIZABLE级别
- 加悲观锁SELECT ... FOR UPDATE
- 使用版本号控制
4. 其他隐蔽的失效场景
4.1 多数据源配置问题
spring: datasource: primary: url: jdbc:mysql://localhost:3306/db1 secondary: url: jdbc:mysql://localhost:3306/db2必须为每个数据源单独配置事务管理器:
@Bean @Primary public PlatformTransactionManager primaryTM(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }4.2 异步方法调用
@Transactional public void asyncOperation() { CompletableFuture.runAsync(() -> { // 这里的事务不会生效 accountDao.update(); }); }5. 诊断事务失效的实用技巧
5.1 日志排查法
在application.yml中开启调试日志:
logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc: TRACE典型的事务生效日志特征:
Creating new transaction with name [...] Acquired Connection [...] for JDBC transaction Initiating transaction commit5.2 测试验证模板
推荐使用这个测试模板验证事务行为:
@SpringBootTest class TransactionTest { @Test void testTransaction() { try { service.transactionalMethod(); } catch (Exception e) { // 验证数据库状态 assertThat(repository.count()).isEqualTo(0); } } }6. 最佳实践总结
注解放置原则:
- 优先加在具体方法而非类上
- 接口上的注解可能不生效(取决于代理方式)
异常处理规范:
@Transactional(rollbackFor = Exception.class) public void safeOperation() throws BusinessException { // ... }性能优化建议:
- 只对需要事务的方法加注解
- 读操作使用@Transactional(readOnly=true)
- 避免长事务(设置timeout参数)
最后分享一个真实案例:某金融系统在批量处理时未设置事务超时,导致数据库连接池耗尽。添加timeout=30后问题解决:
@Transactional(timeout = 30) public void batchProcess() { // 批量操作 }