Spring事务aftercommit原理及实践

来道题

CREATE TABLE `goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `good_id` varchar(20) DEFAULT NULL,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `goods_good_id_index` (`good_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","");

        //part1
        conn.setAutoCommit(false);
        Statement statement = conn.createStatement();
        statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");

        conn.commit();

        //part2
        statement = conn.createStatement();
        statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");

        conn.setAutoCommit(true);

        //part3
        try {
            statement = conn.createStatement();
            statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
            int i = 1/0;
        }catch (Exception ex){
            System.out.println("there is an error");
        }
        conn.setAutoCommit(true);

        //part4
        conn.setAutoCommit(false);
        try {
            statement = conn.createStatement();
            statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);");
            int i = 1/0;
        }catch (Exception ex){
            System.out.println("there is an error");
        }
        conn.setAutoCommit(true);

你举得这4段代码都提交了吗,为什么?

如果你知道这个知识点,那么本文对于你来说很容易理解。

一个知识点

首先,上面4段代码都会提交成功。

主要的知识点是, autocommit 的状态切换时,会对自动提交之前执行的内容。

看下这个方法的注释就知道了。

image.png
image.png

他这边说,如果事务执行过程中,如果 autocommit 状态改变了,会提交之前的事务。

额,这有个逻辑上的问题,如果autocommit本身就是true,我们的语句不是直接就提交了么,那这个描述应该改成从false改成true的时候。

其实这段注释还有前半段。

image.png
image.png

针对DML和DDL语句,autocommit=true的情况下,statement是立刻提交的。

而对于select语句,要等到关联的result set被关闭,对于存储过程....

而这个知识点太偏了,懂的朋友了解下,告诉我是啥..

所以我们这边的知识点严谨点来说就是: 对于DDL和DML语句,当autocommit从false切换为true时,事务会自动提交。

spring事务中的aftercommit

afterCommit是Spring事务机制中的一个回调钩子,用于在事务提交后做一些操作。

我们可以这么使用它

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){
    public void afterCommit() {
        //...执行一些操作
    }
});

也可以通过@TransactionalEventListener间接使用它,它的底层原理就是上面这段代码

@TransactionalEventListener
public void handleEvent(Event event){
    //...执行一些操作
}

重点是在事务提交后执行一些操作,也就是我题目中conn.commit()之后再执行一些操作。

这个时候存在一个问题,如果这个操作是数据库相关的操作,会不会被提交。

根据我文章开篇的代码,你肯定就知道答案就是会提交,但是是autocommit的切换导致的提交。

额,其实并不是,对比2个常用db框架,Mybatis和JPA(Hibernate),Mybatis会提交,而Hibernate会丢失。

image.png
image.png

在afercomit的注释中,他也警告我们了,在aftercommit中做数据库操作可能不会被提交。如果你要做数据库操作,你需要在一个新的事务中,可以使用PROPAGATION_REQUIRES_NEW隔离级别。

源码中的NOTE要仔细的看!!很重点

在不创建新事务的前提下,为什么对于Mybatis和JPA在aftercommit中执行操作,一个提交,一个不提交?开始我们的源码解析。

源码解析

Spring Transaction的核心逻辑封装在TransactionAspectSupportinvokeWithinTransaction方法中,而核心流程中重要的三个操作,获取/提交/回滚事务,由PlatformTransactionManager来实现。

public interface PlatformTransactionManager extends TransactionManager {
    
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
            throws TransactionException;
    
    void commit(TransactionStatus status) throws TransactionException;
    
    void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager使用了策略模式和模板方法模式,它的子类AbstractPlatformTransactionManager又对上面三个方法做了抽象,暴露了一一系列钩子方法让子类实现。

最常用的子类就是DataSourceTransactionManager和HibernateTransactionManager,分别对应Mybatis和JPA框架。

本文讲解的aftercommit同步钩子在AbstractPlatformTransactionManager的processCommit中被触发。

回顾我们上面展示的场景,我们在一个事务里,注册了一个aftercommit钩子,并且aftercommit里面,也会再次操作数据库,执行dml操作。

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            //...
             else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction commit");
                    }
                    unexpectedRollback = status.isGlobalRollbackOnly();
                    //...假设事务提交成功
                    doCommit(status);
             }
            try {
                triggerAfterCommit(status);
            }
            finally {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
            }

        }
        finally {
            cleanupAfterCompletion(status);
        }

在第一个事务doCommit成功,他会通过triggerAfterCommit触发它的aftercommit钩子逻辑,进行下一次事务操作,但是此时的Transaction还没有释放,并且它也不是newTransaction了。

为什么不是newTransaction,见以下代码

private TransactionStatus handleExistingTransaction(
    TransactionDefinition definition, Object transaction, boolean debugEnabled)
    throws TransactionException {
    //...
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}

因为 status.isNewTransaction() 不成立,所以 doCommit(status); 不会执行。

doCommit中会进行什么操作?

对于DataSourceTransactionManager,就是调用了Connection的commit方法,对事务进行提交。

protected void doCommit(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
        logger.debug("Committing JDBC transaction on Connection [" + con + "]");
    }
    try {
        con.commit();
    }
    catch (SQLException ex) {
        throw new TransactionSystemException("Could not commit JDBC transaction", ex);
    }
}

虽然错失了doCommit这个机会,但是在cleanupAfterCompletion(status);方法

private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    status.setCompleted();
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.clear();
    }
    if (status.isNewTransaction()) {
        doCleanupAfterCompletion(status.getTransaction());
    }
    if (status.getSuspendedResources() != null) {
        if (status.isDebug()) {
            logger.debug("Resuming suspended transaction after completion of inner transaction");
        }
        Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
        resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    }
}

在doCleanupAfterCompletion的逻辑中,注意doCleanupAfterCompletion也是一个钩子,这个逻辑也由DataSourceTransactionManager实现

protected void doCleanupAfterCompletion(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;

    // Remove the connection holder from the thread, if exposed.
    if (txObject.isNewConnectionHolder()) {
        TransactionSynchronizationManager.unbindResource(obtainDataSource());
    }

    // Reset connection.
    Connection con = txObject.getConnectionHolder().getConnection();
    try {
        if (txObject.isMustRestoreAutoCommit()) {
            con.setAutoCommit(true);
        }
        DataSourceUtils.resetConnectionAfterTransaction(
            con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
    }
    catch (Throwable ex) {
        logger.debug("Could not reset JDBC Connection after transaction", ex);
    }

    if (txObject.isNewConnectionHolder()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
        }
        DataSourceUtils.releaseConnection(con, this.dataSource);
    }

    txObject.getConnectionHolder().clear();
}

调用到了 con.setAutoCommit(true);间接了提交了事务

然后我们再来看看HibernateTransactionManager对这个两个方法的实现

HibernateTransactionManager#doCommit

protected void doCommit(DefaultTransactionStatus status) {
    HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
    Transaction hibTx = txObject.getSessionHolder().getTransaction();
    Assert.state(hibTx != null, "No Hibernate transaction");
    if (status.isDebug()) {
        logger.debug("Committing Hibernate transaction on Session [" +
                     txObject.getSessionHolder().getSession() + "]");
    }

    try {
        //看这里
        hibTx.commit();
    }
    catch (org.hibernate.TransactionException ex) {
        // assumably from commit call to the underlying JDBC connection
        throw new TransactionSystemException("Could not commit Hibernate transaction", ex);
    }
    catch (HibernateException ex) {
        // assumably failed to flush changes to database
        throw convertHibernateAccessException(ex);
    }
    catch (PersistenceException ex) {
        if (ex.getCause() instanceof HibernateException) {
            throw convertHibernateAccessException((HibernateException) ex.getCause());
        }
        throw ex;
    }
}

HibernateTransactionManager#doCleanupAfterCompletion

protected void doCleanupAfterCompletion(Object transaction) {
    HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;

    // Remove the session holder from the thread.
    if (txObject.isNewSessionHolder()) {
        TransactionSynchronizationManager.unbindResource(obtainSessionFactory());
    }

    // Remove the JDBC connection holder from the thread, if exposed.
    if (getDataSource() != null) {
        TransactionSynchronizationManager.unbindResource(getDataSource());
    }

    Session session = txObject.getSessionHolder().getSession();
    if (this.prepareConnection && isPhysicallyConnected(session)) {
        // We're running with connection release mode "on_close": We're able to reset
        // the isolation level and/or read-only flag of the JDBC Connection here.
        // Else, we need to rely on the connection pool to perform proper cleanup.
        try {
            Connection con = ((SessionImplementor) session).connection();
            Integer previousHoldability = txObject.getPreviousHoldability();
            if (previousHoldability != null) {
                con.setHoldability(previousHoldability);
            }
            DataSourceUtils.resetConnectionAfterTransaction(
                con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
        }
        catch (HibernateException ex) {
            logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
        }
        catch (Throwable ex) {
            logger.debug("Could not reset JDBC Connection after transaction", ex);
        }
    }

    if (txObject.isNewSession()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Closing Hibernate Session [" + session + "] after transaction");
        }
        SessionFactoryUtils.closeSession(session);
    }
    else {
        if (logger.isDebugEnabled()) {
            logger.debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction");
        }
        if (txObject.getSessionHolder().getPreviousFlushMode() != null) {
            session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode());
        }
        if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) {
            disconnectOnCompletion(session);
        }
    }
    txObject.getSessionHolder().clear();
}

docommit里的逻辑还是用到了底层connection的commit,而在doCleanupAfterCompletion中,没有见到设置autocommit的身影。

所以在JPA中你在aftercommit中进行dml操作是会丢失的。

另外一个点是,如果你在aftercommit进行了事务操作,但是中间发生了异常,比如2条insert语句后,发生了异常,这两条insert会不会回滚?

答案是不会

回顾processCommit方法

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            //...
             else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction commit");
                    }
                    unexpectedRollback = status.isGlobalRollbackOnly();
                    //...假设事务提交成功
                    doCommit(status);
             }
            try {
                triggerAfterCommit(status);
            }
            finally {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
            }

        }
        finally {
            cleanupAfterCompletion(status);
        }
}

我们的aftercommit在triggerAfterCommit执行,这个方法里面抛出了异常,因为没有catch,异常会往上传递,在cleanupAfterCompletion里也没有处理异常,但是对于mybatis来讲,它改变了autocommit状态,所以更改被提交了。这是一个你想不到的坑。

最佳实践

  1. aftercommit或者说是transactionlistener,最好不要有dml操作
  2. 一但aftercommit中有事务操作,存在的风险是,一致性得不到保证,异常不会让这部分的事务回滚

demo

写了一个工程,用于测试mybatis和jpa中对于aftercommit中执行dml操作是否会提交

地址如下

https://github.com/shengchaojie/spring_tx_aftercommit_problem

参考资料

https://www.jianshu.com/p/1bfa61868823

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345