Spring 事务管理的一个 trick
问题
最近有同事碰到这个异常信息: 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
上的注释发现修改这个字段并不是解决上面的场景的最佳做法,反而可能引入坑。
注释大意如下:
- 默认是 “true”:如果一个参与事务(例如,标记为的
PROPAGATION_REQUIRES
或PROPAGATION_SUPPORTS
碰到一个已有事务时就是参与事务)失败,事务将被全局地标记为 rollback-only 。这个事务的唯一结果就是回滚:事务发起者再也不能提交事务。- 切换为 “false” 可以让事务发起者决定是否回滚。如果参与事务因为异常失败,调用者仍然可以决定继续走事务内的不同路径。然而,这只有在所有参与资源都允许继续直到事务提交,即使数据访问失败。
- Note:This flag only applies to an explicit rollback attempt for a subtransaction, typically caused by an exception thrown by a data access operation (where TransactionInterceptor will trigger a PlatformTransactionManager.rollback() call according to a rollback rule). If the flag is off, the caller can handle the exception and decide on a rollback, independent of the rollback rules of the subtransaction. This flag does, however, not apply to explicit setRollbackOnly calls on a TransactionStatus, which will always cause an eventual global rollback (as it might not throw an exception after the rollback-only call).
- 处理子事务失败的推荐方案是 嵌套事务:全局事务可以回滚到子事务开始的安全点。
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 .");
}
}
}