Mybatis 中sqlsession原始碼解析
一、sqlsession獲取過程
1、基礎配置
在mybatis框架下進行的資料庫操作都需要首先獲取sqlsession,在mybatis與spring整合後獲取sqlsession需要用到sqlsessionTemplate這個類。
首先在spring對sqlsessionTemplate進行配置,使用到的是 org.mybatis.spring.SqlSessionTemplate 這個類。
<!-- SqlSession例項 --> <bean id="sessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"destroy-method="close"> <!--當建構函式有多個引數時,可以使用constructor-arg標籤的index屬性,index屬性的值從0開始,這裡將sqlsessionFactory作為第一個引數傳入--> <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 將資料來源對映到sqlSessionFactory中 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis/mybatis-config.xml" /> <property name="dataSource" ref="dataSource" /> </bean>
所以在sqlsessionTemplate的初始化過程中,首先會將sqlsessionFactory作為引數傳入,sqlsessionFactory中映射了資料來源資訊。
配置事務,在後來的sqlsession獲取過程中會對事務進行判斷
<!--======= 事務配置 Begin =================--> <!-- 事務管理器(由Spring管理MyBatis的事務) --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 關聯資料來源 --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 註解事務 --> <tx:annotation-driven transaction-manager="transactionManager" /> <!--======= 事務配置 End =================== -->
2、sqlsessionTemplate的初始化
public class SqlSessionTemplate implements SqlSession { private final SqlSessionFactory sqlSessionFactory; private final ExecutorType executorType; private final SqlSession sqlSessionProxy; private final PersistenceExceptionTranslator exceptionTranslator; public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); } public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true)); } public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); Assert.notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor()); }
SqlsessionTemplate類的最開始初始化過程中,首先會通過sqlsessionFactory引數進行構造,通過Proxy.newProxyInstance()方法來建立代理類,表示建立SqlSessionFactory的代理類的例項,該代理類實現SqlSession介面,定義了方法攔截器,如果呼叫代理類例項中實現SqlSession介面定義的方法,該呼叫則被導向SqlsessionTemplate的一個內部類SqlSessionInterceptor的invoke方法,最終初始化sqlsessionProxy。
3、sqlsession的呼叫過程
由於上面兩個過程中已經將sqlsessionTemplate中的sqlsessionProxy已經初始化完畢,所以在程式碼中可以進行呼叫。呼叫最終都會進入SqlSessionInterceptor的invoke方法。
private class SqlSessionInterceptor implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //獲取SqlSession(這個SqlSession才是真正使用的,它不是執行緒安全的) //這個方法可以根據Spring的事物上下文來獲取事物範圍內的sqlSession final SqlSession sqlSession = SqlSessionUtils.getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { //呼叫真實SqlSession的方法 Object result = method.invoke(sqlSession, args); //然後判斷一下當前的sqlSession是否有配置Spring事務 如果沒有自動commit if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { // force commit even on non-dirty sessions because some databases require // a commit/rollback before calling close() sqlSession.commit(true); } //返回執行結果 return result; } catch (Throwable t) { //如果出現異常則根據情況轉換後丟擲 Throwable unwrapped = unwrapThrowable(t); if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped); if (translated != null) { unwrapped = translated; } } throw unwrapped; } finally { //關閉sqlSession //它會根據當前的sqlSession是否在Spring的事務上下文當中來執行具體的關閉動作 //如果sqlSession被Spring事務管理 則呼叫holder.released(); 使計數器-1 //否則才真正的關閉sqlSession SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); } } }
在上面的程式碼中用到兩個很關鍵的方法:
獲取sqlsession方法:SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
關閉sqlsession方法:SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { //根據sqlSessionFactory從當前執行緒對應的資源map中獲取SqlSessionHolder,當sqlSessionFactory建立了sqlSession,就會在事務管理器中新增一對對映:key為sqlSessionFactory,value為SqlSessionHolder,該類儲存sqlSession及執行方式 SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory); //如果holder不為空,且和當前事務同步 if (holder != null && holder.isSynchronizedWithTransaction()) { //hodler儲存的執行型別和獲取SqlSession的執行型別不一致,就會丟擲異常,也就是說在同一個事務中,執行型別不能變化,原因就是同一個事務中同一個sqlSessionFactory建立的sqlSession會被重用 if (holder.getExecutorType() != executorType) { throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction"); } //增加該holder,也就是同一事務中同一個sqlSessionFactory建立的唯一sqlSession,其引用數增加,被使用的次數增加 holder.requested(); //返回sqlSession return holder.getSqlSession(); } //如果找不到,則根據執行型別構造一個新的sqlSession SqlSession session = sessionFactory.openSession(executorType); //判斷同步是否啟用,只要SpringTX被啟用,就是true if (isSynchronizationActive()) { //載入環境變數,判斷註冊的事務管理器是否是SpringManagedTransaction,也就是Spring管理事務 Environment environment = sessionFactory.getConfiguration().getEnvironment(); if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { //如果是,則將sqlSession載入進事務管理的本地執行緒快取中 holder = new SqlSessionHolder(session, executorType, exceptionTranslator); //以sessionFactory為key,hodler為value,加入到TransactionSynchronizationManager管理的本地快取ThreadLocal<Map<Object, Object>> resources中 bindResource(sessionFactory, holder); //將holder, sessionFactory的同步加入本地執行緒快取中ThreadLocal<Set<TransactionSynchronization>> synchronizations registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); //設定當前holder和當前事務同步 holder.setSynchronizedWithTransaction(true); //增加引用數 holder.requested(); } else { if (getResource(environment.getDataSource()) == null) { } else { throw new TransientDataAccessResourceException( "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization"); } } } else { } return session; }
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) { //其實下面就是判斷session是否被Spring事務管理,如果管理就會得到holder SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory); if ((holder != null) && (holder.getSqlSession() == session)) { //這裡釋放的作用,不是關閉,只是減少一下引用數,因為後面可能會被複用 holder.released(); } else { //如果不是被spring管理,那麼就不會被Spring去關閉回收,就需要自己close session.close(); } }
二、mybatis的快取
1、一級快取
Mybatis的一級快取是預設開啟的,主要是通過sqlsession來實現的,每個sqlsession物件會在本地建立一個快取(local cache),對於每次查詢都會在本地快取中進行查詢,如果命中則直接返回,如果沒查到則進入到資料庫中進行查詢。一級快取是sqlsession級別的。
從上面sqlsession的獲取原始碼中可以看到,每次獲取一個全新的sqlsession最終都是會儲存在ThreadLocal中跟執行緒繫結,如果在spring中配置了事務則整個事務週期裡面都共享一個sqlsession,如果沒有配置事務則每次請求都是一個獨立的sqlsession。每次執行完後資料庫操作後,如果還在事務週期中只對sqlsession的引用次數減一,否則直接關閉sqlsession。
一級快取執行的時序圖:
小結:
MyBatis一級快取的生命週期和SqlSession一致。
MyBatis一級快取內部設計簡單,只是一個沒有容量限定的HashMap,在快取的功能性上有所欠缺。
MyBatis的一級快取最大範圍是SqlSession內部,有多個SqlSession或者分散式的環境下,資料庫寫操作會引起髒資料,建議設定快取級別為Statement。
2、二級快取
在系統中如果需要使用二級快取則直接在spring中進行配置宣告即可。
<!-- 這個配置使全域性的對映器啟用或禁用 快取 --> <setting name="cacheEnabled" value="true" /> <!-- 開啟二級快取開關 --> <cache/>
MyBatis的二級快取相對於一級快取來說,實現了SqlSession之間快取資料的共享,同時粒度更加的細,能夠到namespace級別,通過Cache介面實現類不同的組合,對Cache的可控性也更強。
MyBatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用二級快取的條件比較苛刻。
在分散式環境下,由於預設的MyBatis Cache實現都是基於本地的,分散式環境下必然會出現讀取到髒資料,需要使用集中式快取將MyBatis的Cache介面實現,有一定的開發成本,直接使用Redis,Memcached等分散式快取可能成本更低,安全性也更高。