SqlSessionTemplate是如何保證MyBatis中SqlSession的線程安全的?

1、DefaultSqlSession的線程不安全性

在MyBatis架構中SqlSession是提供給外層調用的頂層接口,實現類有:DefaultSqlSession、SqlSessionManager以及mybatis-spring提供的實現SqlSessionTemplate。默認的實現類爲DefaultSqlSession如。類圖結構以下所示: 

對於MyBatis提供的原生實現類來講,用的最多的就是DefaultSqlSession,但咱們知道DefaultSqlSession這個類不是線程安全的!以下: 
web

2、SqlSessionTemplate是如何使用DefaultSqlSession的

而在咱們開發的時候確定會用到Spring,也會用到mybatis-spring框架,在使用MyBatis與Spring集成的時候咱們會用到了SqlSessionTemplate這個類,例以下邊的配置,注入一個單例的SqlSessionTemplate對象:面試

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg ref="sqlSessionFactory" />
</bean>

SqlSessionTemplate的源代碼註釋以下: 


經過源碼咱們何以看到 SqlSessionTemplate實現了SqlSession接口,也就是說咱們可使用SqlSessionTemplate來代理以往的DefaultSqlSession完成對數據庫的操做,可是DefaultSqlSession這個類不是線程安全的,因此DefaultSqlSession這個類不能夠被設置成單例模式的。spring

若是是常規開發模式的話,咱們每次在使用DefaultSqlSession的時候都從SqlSessionFactory當中獲取一個就能夠了。可是與Spring集成之後,Spring提供了一個全局惟一的SqlSessionTemplate對象來完成DefaultSqlSession的功能,問題就是:不管是多個Dao使用一個SqlSessionTemplate,仍是一個Dao使用一個SqlSessionTemplate,SqlSessionTemplate都是對應一個sqlSession對象,當多個web線程調用同一個Dao時,它們使用的是同一個SqlSessionTemplate,也就是同一個SqlSession,那麼它是如何確保線程安全的呢?讓咱們一塊兒來分析一下:sql

3、SqlSessionTemplate是如何保證DefaultSqlSession線程安全的

(1)首先,經過以下代碼建立代理類,表示建立SqlSessionFactory的代理類的實例,該代理類實現SqlSession接口,定義了方法攔截器,若是調用代理類實例中實現SqlSession接口定義的方法,該調用則被導向SqlSessionInterceptor的invoke方法(代理對象的InvocationHandler就是SqlSessionInterceptor,若是把它命名爲SqlSessionInvocationHandler則更好理解!) 

核心代碼就在 SqlSessionInterceptor的invoke方法當中。數據庫

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
            throws Throwable {
        //獲取SqlSession(這個SqlSession纔是真正使用的,它不是線程安全的)
        //這個方法能夠根據Spring的事務上下文來獲取事物範圍內的sqlSession
        SqlSession sqlSession = getSqlSession(
                SqlSessionTemplate.this.sqlSessionFactory,
                SqlSessionTemplate.this.executorType,
                SqlSessionTemplate.this.exceptionTranslator);
        try {
            //調用從Spring的事物上下文獲取事物範圍內的sqlSession對象
            Object result = method.invoke(sqlSession, args);
            //而後判斷一下當前的sqlSession是否被Spring託管 若是未被Spring託管則自動commit
            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) {
            //若是出現異常則根據狀況轉換後拋出
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null && 
                    unwrapped instanceof PersistenceException) {
                // release the connection to avoid a deadlock if the 
                // translator is no loaded. See issue #22
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                sqlSession = null;
                Throwable translated = SqlSessionTemplate.this.exceptionTranslator.
                        translateExceptionIfPossible((PersistenceException) unwrapped);
                if (translated != null) {
                    unwrapped = translated;
                }
            }
            throw unwrapped;
        } finally {
            if (sqlSession != null) {
                //關閉sqlSession,它會根據當前的sqlSession是否在Spring的事物上下文當中來執行具體的關閉動做
                //若是sqlSession被Spring管理 則調用holder.released();
                //使計數器-1,不然才真正的關閉sqlSession
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }
        }
    }
}

在上面的invoke方法當中使用了兩個工具方法分別是:安全

(1)SqlSessionUtils.getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)
(2)SqlSessionUtils.closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory)

那麼這兩個方法又是如何與Spring的事物進行關聯的呢?session

一、getSqlSession方法以下:mybatis

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, 
        ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    //根據sqlSessionFactory從當前線程對應的資源map中獲取SqlSessionHolder,
    // 當sqlSessionFactory建立了sqlSession,
    //就會在事務管理器中添加一對映射:key爲sqlSessionFactory,value爲SqlSessionHolder,
    // 該類保存sqlSession及執行方式
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.
            getResource(sessionFactory);

    //從SqlSessionHolder中提取SqlSession對象
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    }

    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Creating a new SqlSession");
    }
    //若是當前事物管理器中獲取不到SqlSessionHolder對象就從新建立一個
    session = sessionFactory.openSession(executorType);

    //將新建立的SqlSessionHolder對象註冊到TransactionSynchronizationManager中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
}

二、closeSqlSession方法以下:架構

//刪除了部分日誌代碼
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
        //其實下面就是判斷session是否被Spring事務管理,若是管理就會獲得holder
        SqlSessionHolder holder = (SqlSessionHolder) 
        TransactionSynchronizationManager.getResource(sessionFactory);
        if ((holder != null) && (holder.getSqlSession() == session)) {
            //這裏釋放的做用,不是關閉,只是減小一下引用數,由於後面可能會被複用 
            holder.released();
        } else {
            //若是不是被spring管理,那麼就不會被Spring去關閉回收,就須要本身close 
            session.close();
        }
    }

大體的分析到此爲止,可能有些許不夠順暢,不過:紙上得來終覺淺,絕知此事要躬行!還但願小夥伴打開本身的編譯器,找到此處的代碼,認真走一遍流程!app

其實經過上面的代碼咱們能夠看出Mybatis在不少地方都用到了代理模式,代理模式能夠說是一種經典模式,其實不牢牢在這個地方用到了代理模式,Spring的事物、AOP、Mybatis數據庫鏈接池技術、MyBatis的核心原理(如何在只有接口沒有實現類的狀況下完成數據庫的操做!)等技術都使用了代理技術。

4、SqlSessionManager又是什麼鬼?

上述說了SqlSession的實現還有一個SqlSessionManager,那麼SqlSessionManager究竟是什麼個東西哪?且看定義以下: 

你可能會發現SqlSessionManager的構造方法居然是private的,那咱們怎麼建立這個對象哪?其實SqlSessionManager建立對象是經過newInstance的方法建立對象的,但須要注意的是他雖然有私有的構造方法,而且提供給咱們了一個公有的newInstance方法,但它並非一個單例模式!

newInstance有不少重載的方法,以下所示: 

SqlSessionManager的openSession方法及其重載的方法是直接經過調用其中底層封裝的SqlSessionFactory對象的openSession方法來建立SqlSession對象的,重載方法以下: 

SqlSessionManager中實現了SqlSession接口中的方法,例如:select、update等,都是直接調用sqlSessionProxy代理對象中相應的方法。在建立該代理對像的時候使用的InvocationHandler對象是SqlSessionInterceptor,他是定義在SqlSessionManager的一個內部類,其定義以下: 

5、總結

綜上所述,咱們應該大體瞭解了DefaultSqlSession和SqlSessionManager之間的區別:

一、DefaultSqlSession的內部沒有提供像SqlSessionManager同樣經過ThreadLocal的方式來保證線程的安全性

二、SqlSessionManager是經過localSqlSession這個ThreadLocal變量,記錄與當前線程綁定的SqlSession對象,供當前線程循環使用,從而避免在同一個線程屢次建立SqlSession對象形成的性能損耗;

三、DefaultSqlSession不是線程安全的,咱們在進行原生開發的時候,須要每次爲一個操做都建立一個SqlSession對象,其性能可想而知;

6、擴展面試題

那麼問題來了:

一、爲何mybatis-spring框架中不直接使用線程安全的SqlSessionManager(SqlSessionFactory它是線程安全的)而是使用DefaultSqlSession這個線程不安全的類,並經過動態代理的方式來保證DefaultSqlSession操做的線程安全性哪?

二、DefaultSqlSession中是如何經過Executor來表現策略模式的或者DefaultSqlSession如何使用策略模式模式的?

相關文章
相關標籤/搜索