最近有同事碰到这个异常信息: Transaction rolled back because it has been marked as rollback-only
,异常栈被吃了,没打印出来。
调用代码大概如下:
@Component public class InnerService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional(rollbackFor = Throwable.class) public void innerTx(boolean ex) { jdbcTemplate.execute("insert into t_user(uname, age) values('liuwhb', 31)"); if (ex) { throw new NullPointerException(); } } } @Component public class OutterService { @Autowired private InnerService innerService; @Transactional(rollbackFor = Throwable.class) public void outTx(boolean ex) { try { innerService.innerTx(ex); } catch (Exception e) { e.printStackTrace(); } } } outterService.outTx(true);
他期望的是 innerService.innerTx(ex);
调用即使失败了也不会影响 OutterService.outTx
方法上的事务,只回滚了 innerTx
的操作。
结果没有得到他想要的,调用 OutterService.outTx
的外围方法捕获到了异常,异常信息是 Transaction rolled back because it has been marked as rollback-only
, outTx
的其他操作也没有提交事务。
上述方法的事务传播机制的默认的,也就是 Propagation.REQUIRED
,如果当前已有事务就加入当前事务,没有就新建一个事务。
事务性的方法 outTx
调用了另一个事务性的方法 innerTx
。调用方对被调用的事务方法进行异常捕获,目的是希望被调用方的异常不会影响调用方的事务。
但还是会影响调用方的行为的。Spring 捕获到被调用事务方法的异常后,会把事务标记为 read-only
,然后调用方提交事务的时候发现事务是只读的,就会抛出上面的异常。
网上有人把 AbstractPlatformTransactionManager.globalRollbackOnParticipationFailure
属性设置为 false
,说也把问题解决了。
仔细看了更新这个字段的方法 AbstractPlatformTransactionManager.setGlobalRollbackOnParticipationFailure
上的注释发现修改这个字段并不是解决上面的场景的最佳做法,反而可能引入坑。
注释大意如下:
PROPAGATION_REQUIRES
或 PROPAGATION_SUPPORTS
碰到一个已有事务时就是参与事务)失败,事务将被全局地标记为 rollback-only 。这个事务的唯一结果就是回滚:事务发起者再也不能提交事务。 PROPAGATION_NESTED
提供了这种语义。当然,这个只有在使用支持嵌套事务的 DataSourceTransactionManager
时生效, JtaTransactionManager
不支持嵌套事务。
之所以说修改这个字段可能引入坑是因为:容易让人误以为 innerTx
抛出异常后,它做的操作就被回滚了,其实是没有的,这些操作会跟随 outTx
上的事务一起提交。也就是说 innerTx
里的操作可能只完成了一部分,这就破外了事务的完整性。
把 innerTx
标记为 @Transactional(propagation = Propagation.NESTED)
可以保证 innerTx
里操作的事务完整性。
这其实是个嵌套事务的处理场景。
其他的测试代码:
DROP TABLE IF EXISTS t_user; create table t_user(uid int auto_increment , uname VARCHAR(100), age int, PRIMARY KEY(uid) ) ENGINE = INNODB default CHARSET UTF8;
@SpringBootApplication public class TxApp { private static Logger logger = LoggerFactory.getLogger(TxApp.class); @Bean public PlatformTransactionManager initTransactionManager(DataSource ds) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(ds); transactionManager.setGlobalRollbackOnParticipationFailure(false); return transactionManager; } public static void main(String[] args) { try (ConfigurableApplicationContext ctx = SpringApplication.run(TxApp.class, args)) { OutterService outterService = ctx.getBean(OutterService.class); // outterService.outTx(false); outterService.outTx(true); logger.info("outTx commit success ."); } } }