mybatis中的事務管理
mybatis中的事務控制
mybatis中執行sql是從SqlSession
開始的,SqlSession
中提供了各種操作資料庫的方法
SqlSession
中持有執行器Executor
物件,通過執行器來執行sql
mybatis事務的本質是通過connection實現的,通過connection控制事務的提交,回滾,只有通過同一個connection執行的sql才能被控制住
一、mybatis單獨使用的情況
public class Test02 { public static void main(String[] args) throws IOException { InputStream resource = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource); //獲取sqlsession SqlSession sqlSession = sqlSessionFactory.openSession(); //通過sqlsession執行sql sqlSession.insert("com.lyy.mybatis_source.mapper.BankMapper.insert"); //提交事務 sqlSession.commit(); } }
單獨使用mybatis時,一般會按上邊的步驟來進行。其中openSession
方法如果傳true創建出的sqlsession會自動提交事務,傳false或者不傳得到的sqlsession需要手動呼叫commit方法來提交事務
SqlSessionFactory
是一個介面,SqlSessionFactoryBuilder.build
方法得到的是其實現類DefaultSqlSessionFactory
的物件,openSession方法會得到DefaultSqlSession
物件,下面來分析下
DefaultSqlSession
的commit方法,
@Override public void commit() { commit(false); } @Override public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
可以看到這個commit方法最終會呼叫執行器executor的commit方法.執行器Executor
是一個介面,常用的實現類有三個SimpleExecutor
,ReuseExecutor
,BatchExecutor
,
sqlSessionFactory.openSession方法呼叫的時候可以指定執行器的型別,如果不指定建立的是SimpleExecutor
所以繼續分析這個執行器的commit方法,其呼叫的是父類BaseExecutor
中的方法
@Override public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } }
可以看到最後呼叫的是transaction.commit(),這個transaction是BaseExecutor
類中的一個屬性
protected Transaction transaction;
Transaction
是mybatis定義的一個介面,其中提供了獲取連線,操作事務等的方法,
public interface Transaction {
Connection getConnection() throws SQLException;
void commit() throws SQLException;
void rollback() throws SQLException;
void close() throws SQLException;
Integer getTimeout() throws SQLException;
}
執行時使用哪個實現類取決於
mybatis配置檔案中指定的transactionManager型別
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/transaction_test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
這個配置檔案中指定的是JDBC,所以最後會使用JdbcTransaction
這個實現類
繼續看這個類中的commit方法
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
其中connection是這個類中的一個屬性,代表的是執行sql時使用的資料庫連線,這裡通過連線物件執行commit方法來提交事務,並且如果這個連線設定的是自動提交事務這個方法就什麼都不做。
總結下來就是sqlSession.commit方法最終會通過Transaction
中的connection來提交事務。
sqlsession--->Executor-->Transaction-->connection
可以推斷,sqlsession中執行sql時肯定也是呼叫這個Transaction.getConnection
來獲取連線,這樣才能保證執行sql時和提交事務時使用的是同一個連線
JdbcTransaction
中獲取連線的方法
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
再來思考一個問題,Executor
中的Transaction是什麼時候賦值的?
這就需要看下DefaultSqlSessionFactory.openSession()方法,
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// configuration是DefaultSqlSessionFactory中的一個屬性
final Environment environment = configuration.getEnvironment();
// 解析配置檔案的過程中會創建出transactionFactory,並設定給environment
// 再把environment設定到configuration物件的屬性上
final TransactionFactory transactionFactory =
getTransactionFactoryFromEnvironment(environment);
// 通過transactionFactory來獲取Transaction物件
// 基於我們的配置檔案這裡使用的是JdbcTransactionFactory
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//建立 Executor物件時傳入了tx物件和執行器型別
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我們mybatis配置檔案中配置的事務型別是JDBC,所以這裡使用的TransactionFactory是 JdbcTransactionFactory
public class JdbcTransactionFactory implements TransactionFactory {
@Override
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
}
所以當我們使用同一個sqlSession來執行多條sql時,因為每次都是按
sqlsession--->Executor-->Transaction-->connection 這樣的方式去獲取資料庫連線,所以第一次執行時會獲取一個新連線,後續的執行都是從Transaction中拿到已有的connection執行sql,保證了使用同一個connection,
最終再通過這個connection來提交事務
二、mybatis和spring整合的情況
mybatis官方提供了一個mybatis-spring
包可以用來整合spring。
整合spring後,最終的sql語句還是要通過SqlSession
來執行的,也就是DefaultSqlSession
,只不過為了和spring整合做了幾層代理,所以從容器中獲取的sqlSession是SqlSessionTemplate
型別的,這個類也實現了
SqlSession介面
public class SqlSessionTemplate implements SqlSession, DisposableBean {
// 這是sqlsession的代理
private final SqlSession sqlSessionProxy;
//這是構造方法
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//這是在構造方法中給sqlsession的代理賦值
//具體邏輯是在SqlSessionInterceptor中實現,它是當前類中的內部類
this.sqlSessionProxy =
(SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
//內部類
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在這個invoke方法中才會真正的去獲取Sqlession,然後用sqlsession去執行sql
//這裡獲取到的肯定也是 DefaultSqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
//執行方法,可以看做是對上邊獲取到的sqlSession物件中的方法用動態代理來做增強
Object result = method.invoke(sqlSession, args);
//這些就是增強邏輯
//這個判斷的意思是如果沒有用spring來控制事務,就在這裡使用sqlSession提交事務
//如果spring控制事務,這裡不做操作讓spring統一管理事務
if (!isSqlSessionTransactional(sqlSession,
SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
} catch {
//省略
} finally {
//省略
}
}
}
}
這個 getSqlSession方法是mybatis-spring包中提供的工具類SqlSessionUtils中的方法
SqlSessionUtils
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
// TransactionSynchronizationManager是spring提供的一個同步器,裡邊有許多ThreadLocal,
// 通過它可以保證同一個執行緒範圍內多次獲取得到的是同一個sqlsession
// 它裡邊以鍵值對的形式存資料,在這裡sessionFactory就是鍵,
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
//第一次訪問時需要新開啟一個Sqlsession然後註冊到同步器中,下次訪問就可以直接獲取
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
上邊的程式碼保證了呼叫dao方法執行sql時,同一個執行緒範圍內使用的都是同一個sqlsession物件,
再根據第一部分的結論 sqlsession--->Executor-->Transaction 就可以保證每次獲取的都是同一個transaction。
那麼思考一個問題,spring怎麼來控制這種情況的事務?
分析下,要控制事務,spring必須獲取到connection,而從上邊的分析connection被封裝在Transaction中,spring如何獲取到connection呢
mybatis-spring中也提供了一個Transaction的實現類,SpringManagedTransaction
這種情況下使用的是這個實現類,看下其中獲取connection的方法
SpringManagedTransaction中的原始碼
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}
同樣的,只有第一次呼叫會新建,其餘直接返回。繼續看DataSourceUtils.getConnection
這是spring-jdbc提供的一個獲取連線的工具類,也就是mybatis是通過spring來獲取connect,這樣就有機會把這個connection再暴露給spring,後邊spring就可以通過這個connection來管理事務
public abstract class DataSourceUtils {
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
//呼叫這個方法去真正獲取連線
return doGetConnection(dataSource);
} catch (SQLException var2) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
} catch (IllegalStateException var3) {
throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//可以看到是先從同步器TransactionSynchronizationManager中獲取連線
// 這裡邊有ThreadLocal可以存資料庫連線,第一次獲取建立一個connection存到同步器裡
// 下次再訪問時就可以從同步器中直接取。
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
logger.debug("Fetching JDBC Connection from DataSource");
//進到這裡表示是第一次獲取連線,這個方法中會從dataSource獲取連線
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
try {
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
//註冊connection到同步器中,下次可以直接獲取
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
} catch (RuntimeException var4) {
releaseConnection(con, dataSource);
throw var4;
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
return conHolder.getConnection();
}
}
}
總結下來這個方法會先從同步器TransactionSynchronizationManager
中獲取連線,如果獲取不到就新建一個再放進去,而這個類是spring提供的,所以spring事務管理模組可以通過這個類來獲取連線。
所以總結下spring+mybatis的事務管理流程,對於一個有事務的service方法,
(1) 剛開始肯定是spring先開啟事務,開啟事務就是獲取連線,設定連線的autoCommit為false,然後會把連線跟當前執行緒繫結,設定到同步器TransactionSynchronizationManager
中
(2) mybatis執行sql時按照
sqlsession--->Executor-->Transaction-->DataSourceUtils-->TransactionSynchronizationManager
-->connection 就可以獲取到spring開啟事務時放進去的那個connection,然後執行sql
(3) 不管中間執行了多少sql,因為同一個執行緒內使用的是同一個sqlSesssion,所以
都是通過同一個connection執行的
(4) spring從TransactionSynchronizationManager
中獲取到connection提交或回滾事務
在補充兩點,上邊講到spring+mybaits時執行sql使用的是SqlSessionTemplate
,因為它實現了Sqlsession介面,所以其中也有commit方法,但直接調這個方法會拋異常,官方不讓這樣呼叫
@Override
public void commit() {
throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
}
那如果我整合了spring後直接通過DefaultSqlSession來呼叫commit方法會怎麼樣呢?
我們思考下這個commit方法最終調的是Transaction
中的commit,此時實現類是SpringManagedTransaction
,
看下其原始碼
@Override
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
this.connection.commit();
}
}
注意這裡頭有個isConnectionTransactional變數,當spring事務開啟時,走到這個方法時這個變數會是true,所以不進if,這個方法什麼都不做。還是會讓spring來管理事務,調sqlsession.commit方法是無效的。
至於spring是如何進行事務管理的,這又是一大內容,後邊再具體描述。簡單來講,spring的事務管理是基於aop
實現的,方法執行時實現事務功能的入口在TransactionInterceptor
這個類的invoke
方法中,
然後會執行到TransactionAspectSupport#createTransactionIfNecessary
-->
AbstractPlatformTransactionManager#getTransaction
-->startTransaction