最近有同学在项目中遇到了多数据源情况下事务无法生效的问题,所以想写一篇关于多数据源下使用事务的文章。但是在讲解事务之前有一个知识点必须先了解下,那就是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(4, 1, 2) && 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)null, 1003, 1007, false, this.database, (Field[])null, false);}}}
结论:数据库的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设置成defaultAutoCommitif (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方法提交
//此方法位于SpringManagedTransactionpublic 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。
@Overrideprotected 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,就将它设置成falsetxObject.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
@Overrideprotected 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);}}
@Overrideprotected 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会帮我们提交。
近期热文




