暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

聊一聊熟悉又陌生的autoCommit

codeImport 2020-03-08
5934

最近有同学在项目中遇到了多数据源情况下事务无法生效的问题,所以想写一篇关于多数据源下使用事务的文章。但是在讲解事务之前有一个知识点必须先了解下,那就是autoCommit。

很多同学都知道mysql有一个参数叫autoCommit,数据库连接池也可以设置这个参数,但是如果要问设置了这个参数对应用程序有什么影响,很多人就不知道了。

如果设置了autoCommit=false,是不是我的程序就必须要手动提交了?其实这个参数在我们每次执行sql的时候都会起作用,特别当我们在开启事务时,对这个参数的控制尤为重要。因此,了解这个参数在应用程序的作用是十分必要的。

这篇文章将会解答以下问题:

1.如果设置数据库的autoCommit=false,应用程序需要手动提交吗?2.如果设置连接池的autoCommit=false,应用程序需要手动提交吗?3.如果设置连接池的autoCommit=false,又没有事务,Mybatis会帮我们提交吗?4.如果设置连接池的autoCommit=true,但是连接又处于事务中,连接会在什么时候提交?5.什么时候需要手动提交?

以下分析所使用的环境为:

数据库:mysql 5.7.25,

数据库驱动:mysql-connector 5.1.47,

DAO层:mybatis 3.4.6,

应用层:spring 5.0.10,

连接池:druid 1.0.28

1,数据库的autoCommit对应用程序有影响吗?

网上有文章说,连接池中的连接autoCommit属性默认跟数据库中的autoCommit相同。真的是这样的吗?答案是否定的,连接池中的连接是通过数据库驱动生成的,数据库驱动在初始化连接时会无视数据库的autoCommit属性,将该连接设置成autoCommit=1。

Mysql驱动生成的连接为ConnectionImpl,该连接在初始化后会设置autoCommit=1;

//Mysql连接初始化时,会调用该方法,随后调用setAutoCommit命令
//设置aucommit=1;
private void handleAutoCommitDefaults() throws SQLException {
       boolean resetAutoCommitDefault = false;
       if (!this.getElideSetAutoCommits()) {
String initConnectValue = (String)this.serverVariables.get("init_connect");
           if (this.versionMeetsMinimum(412) && initConnectValue != null && initConnectValue.length() > 0) {
// 如果数据库版本小于4.1.2的逻辑,小于也会设置成true,只是设置方法不同
           }else{
resetAutoCommitDefault = true;
           }
       }
       if (resetAutoCommitDefault) {
           try {
this.setAutoCommit(true);
           } catch (SQLException var17) {
               if (var17.getErrorCode() != 1820 || this.getDisconnectOnExpiredPasswords()) {
throw var17;
}
}
}
//调用setAutoCommit方法执行 SET autocommit=1命令
public void setAutoCommit(final boolean autoCommitFlag) throws SQLException {
    synchronized(this.getConnectionMutex()) {
        if (needsSetOnServer) {
               this.execSQL((StatementImpl)null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, (Buffer)null10031007falsethis.database, (Field[])nullfalse);
            }
}
 }

结论:数据库的autoCommit对应用程序没有影响,驱动生成的连接都会自动提交。

2,连接池设置了autoCommit对应用程序有什么影响?

Druid连接池有一个属性:defaultAutoCommit,在数据库驱动生成好连接后,连接池会判断该属性是否与连接的autoCommit相等,如果不相等则会将连接的autoCommit设置为defaultAutoCommit。上文说到数据库连接初始化后的autoCommit都是等于true,如果此时defaultAutoCommit等于false,则会将数据库连接设置为false。

//此代码位于DruidAbstractDataSource,会在驱动生成完连接后调用    
public void initPhysicalConnection(Connection conn, Map<String, Object> variables, Map<String, Object> globalVariables) throws SQLException {
    //判断如果连接池的defaultAutoCommit与生成的连接的autoCommit不相等
    //则将连接的autoCommit设置成defaultAutoCommit
        if (conn.getAutoCommit() != defaultAutoCommit) {
conn.setAutoCommit(defaultAutoCommit);
        }

结论:连接池的autoCommit对应用程序有影响,如果连接池设置了defaultAutoCommit=false,则需要手动提交。

3,如果设置了defaultAutoCommit=false,我们真的需要手动提交吗?

上文说到如果数据库连接池设置了defaultAutoCommit=false,则需要手动提交。但通常情况下,我们都不会直接操作数据库,而是使用Mybatis这样的Dao层中间件帮我们处理对数据库的增删改查。那如果Mybatis遇到连接池设置自动提交为false的情况下,它会帮我们提交吗?答案是肯定的,Mybatis不仅会帮我们执行sql,还会帮我们搽屁股。

Mybatis执行sql的入口位于SqlSessionTemplate的内部类SqlSessionInterceptor,该类的invoke方法在执行完sql后会判断当前是否处于事务中,如果没有,则会调用sqlSession.commit方法提交修改。

   private class SqlSessionInterceptor implements InvocationHandler {
       private SqlSessionInterceptor() {
        }
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
            try {
               Object result = method.invoke(sqlSession, args);
               //执行完数据库操作后,会判断是否处于事务中
               //如果没有处于事务,会调用SqlSession提交
               if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                   sqlSession.commit(true);
               }

最终后调用到SpringManagedTransaction的commit方法提交

//此方法位于SpringManagedTransaction
public void commit() throws SQLException {
       if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
           //此处只会在当前连接不处于事务中,并且autoCommit=false的情况下提交
           //正好对应连接池的autoCommit=false的情况
           this.connection.commit();
}
}

结论:如果连接池的autoCommit=false,但是Dao层使用了Mybatis,则不用手动提交。

4. 如果连接池设置autoCommit=true,但是又有事务,需要我们手动提交吗?

上文说到Mybatis会帮我们手动提交,但是只限于该连接不处于事务的情况下。那如果有事务的情况下,程序又是怎么提交的呢?在使用Spring的情况下,当遇到事务,会首先执行事务管理器(默认是DataSourceTransactionManager)的doBegin方法,该方法的作用是从连接池获取连接,并把该连接放入了事务上下文中,最后设置了该连接的autoCommit=false

   @Override
   protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;
       try {
           if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
               //从连接池获取连接
               Connection newCon = obtainDataSource().getConnection();
               if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
           }


            // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
            // so we don't want to do it unnecessarily (for example if we've explicitly
            // configured the connection pool to set it already).
            if (con.getAutoCommit()) {
//如果该连接的autoCommit=true,就将它设置成false
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
            }


            //Bind the connection holder to the thread.
            if (txObject.isNewConnectionHolder()) {
                //将连接放入事务管理器上下文,以后同一个事务都会使用同一条连接
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}

从doBegin方法可以看出,开启事务最重要的一步就是将连接autoCommit设置成了false。在事务执行完毕后会调用事务管理器的doCommit方法提交事务,并将连接的autoCommit从新设置成true

    @Override
    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);
}
}
    @Override
    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()) {
                //重置连接的autoCommit属性
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
        catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
        }
}

结论:当有事务的情况下,spring在开启事务的时候会先将连接的autoCommit设置成false,在执行完事务后会自动提交事务,并将autoCommit重置成true。

5.什么时候需要手动提交?

经过上面的分析,我们知道不管autoCommit的值如何,我们都不需要手动提交,那什么时候需要我们手动提交呢?答案是你手动开启事务的时候,就需要你手动提交事务。但是不建议手动开启事务,除非你真的知道自己在做什么。好比家里已经有了一个5星级大厨,你为什么非要自己动手做饭呢?

6.总结

从以上分析可以看出,为了帮助我们正确的提交,避免数据库死锁,各个组件真是替我们操碎了心。首先是数据库驱动生成连接时,统一设置该连接为自动提交。其次,如果不小心设置了连接池为不自动提交,Mybatis也会专门针对这种情况帮我们提交。最后,如果我们开启了事务,Spring会帮我们提交。


近期热文

Dubbo异步调用

Dubbo线程池配置详解

Dubbo限流源码分析

文章转载自codeImport,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论