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-onlyoutTx 的其他操作也没有提交事务。

分析

上述方法的事务传播机制的默认的,也就是 Propagation.REQUIRED,如果当前已有事务就加入当前事务,没有就新建一个事务。

事务性的方法 outTx 调用了另一个事务性的方法 innerTx 。调用方对被调用的事务方法进行异常捕获,目的是希望被调用方的异常不会影响调用方的事务。

但还是会影响调用方的行为的。Spring 捕获到被调用事务方法的异常后,会把事务标记为 read-only,然后调用方提交事务的时候发现事务是只读的,就会抛出上面的异常。

解决方法

网上有人把 AbstractPlatformTransactionManager.globalRollbackOnParticipationFailure 属性设置为 false,说也把问题解决了。

仔细看了更新这个字段的方法 AbstractPlatformTransactionManager.setGlobalRollbackOnParticipationFailure 上的注释发现修改这个字段并不是解决上面的场景的最佳做法,反而可能引入坑。

注释大意如下:

  • 默认是 “true”:如果一个参与事务(例如,标记为的 PROPAGATION_REQUIRESPROPAGATION_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 .");
        }
    }

}

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据