問題就是:不管是多個dao使用一個SqlSessionTemplate,仍是一個dao使用一個SqlSessionTemplate,SqlSessionTemplate都是對應一個sqlSession,當多個web線程調用同一個dao時,它們使用的是同一個SqlSessionTemplate,也就是同一個SqlSession,如何保證線程安全,關鍵就在於代理:web
(1)首先,經過以下代碼建立代理類,表示建立SqlSessionFactory的代理類的實例,該代理類實現SqlSession接口,定義了方法攔截器,若是調用代理類實例中實現SqlSession接口定義的方法,該調用則被導向SqlSessionInterceptor的invoke方法spring
- this.sqlSessionProxy = (SqlSession) newProxyInstance(
- SqlSessionFactory.class.getClassLoader(),
- new Class[] { SqlSession.class },
- new SqlSessionInterceptor());
(2)因此關鍵之處轉移到invoke方法中,代碼以下,該類的註釋是代理將Mybatis的方法調用導向從Spring的事務管理器獲取的合適的SqlSession,說明雖然都是調用一樣一個SqlSession接口,可是實際執行sql的sqlSession會有所不一樣。sql
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- //調用SqlSessionUtils的getSqlSession方法從Spring的事務管理器獲取合適的SqlSession
- final SqlSession sqlSession = getSqlSession(
- SqlSessionTemplate.this.sqlSessionFactory,
- SqlSessionTemplate.this.executorType,
- SqlSessionTemplate.this.exceptionTranslator);
- try {
- //經過sqlSession對象調用該方法
- Object result = method.invoke(sqlSession, args);
- //判斷sqlSession是否被Spring事務管理,也就是sqlSession被放在Spring事務管理的本地線程緩存中。若是不是,則須要本身提交。若是是,則Spring經過代理機制,進行提交和回滾
- if (!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) {
- //若是出現異常,則利用異常轉換器將Mybatis的異常轉爲Spring的DataAccessException
- 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鏈接
- closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
- }
- }
其中,核心有兩部分:數據庫
(3)如何從Spring的事務管理器中得到合適的sqlSession,從而保證線程安全,很明顯全部dao的多個線程不是使用同一個sqlSession,否則其中一個closeSqlSession,其餘怎麼用。緩存
該方法的註釋:從Spring事務管理器中獲得一個SqlSession,若是須要建立一個新的。首先努力從當前事務以外獲得一個SqlSession,若是沒有就創造一個新的。而後,若是Spring TX被激活,也就是事務被打開,且事務管理器是SpringManagedTransactionFactory時,將獲得的SqlSession同當前事務同步,下面是該函數的核心代碼安全
- 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;
- }
上述代碼中能夠看出,只有在一個線程的一個事務中,由同一個sqlSessionFactory建立的執行類型相同的sqlSession纔會被複用,其餘狀況下都是建立新的sqlSession。試想一下,即便只有一個SqlSessionTemplate供全部dao使用,全部地方使用的都是同以個sqlSessionFactory,可是因爲是不一樣線程,因此獲得的不是同一個sqlSession,所以不會出現線程安全問題。好處是同一個線程同一個事務中sqlSession會被複用,不會每執行一個sql請求,都建立一個SqlSession,這樣很浪費資源,由於SqlSession至關於一次數據庫鏈接。session
(4)如何關閉sqlSession鏈接,主要代碼以下app
- 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();
- }
- }