如何保證SqlSession的線程安全?

DefaultSqlSession是線程不安全的

在Mybatis中SqlSession是提供給外部調(diào)用的頂層接口局骤,實(shí)現(xiàn)類有:DefaultSqlSession鹃愤、SqlSessionManager以及mybatis-spring提供的實(shí)現(xiàn)SqlSessionTemplate。默認(rèn)實(shí)現(xiàn)類為DefaultSqlSession如绸,是線程不完全的嘱朽。類結(jié)構(gòu)圖如下:

file

對于Mybatis提供的原生實(shí)現(xiàn)類來說,用的最多就是DefaultSqlSession怔接,但是我們知道DefaultSqlSession這個類不是線程安全的搪泳!如下:

file

SqlSessionTemplate是如何保證線程安全的

在我們平時的開發(fā)中通常會用到Spring,也會用到mybatis-spring框架扼脐,在Spring集成Mybatis的時候我們可以用到SqlSessionTemplate(Spring提供的SqlSession實(shí)現(xiàn)類)案训,使用場景案例如下:

file

查看SqlSessionTemplate的源碼注釋如下:

file

通過源碼注釋可以看到SqlSessionTemplate是線程安全的類布轿,并且實(shí)現(xiàn)了SqlSession接口球凰,也就是說我們可以通過SqlSessionTemplate來代替以往的DefaultSqlSession完成對數(shù)據(jù)庫CRUD操作虐沥,并且還保證單例線程安全,那么它是如何保證線程安全的呢脏榆?

首先猖毫,通過SqlSessionTemplate擁有的三個重載的構(gòu)造方法分析,最終都會調(diào)用最后一個構(gòu)造方法须喂,會初始化一個SqlSessionProxy的代理對象吁断,如果調(diào)用代理類實(shí)例中實(shí)現(xiàn)的SqlSession接口中定義的方法,該調(diào)用會被導(dǎo)向SqlSessionInterceptor的invoke方法觸發(fā)代理邏輯

file

接下來查看SqlSessionInterceptor的invoke方法

  1. 通過getSqlSession方法獲取SqlSession對象(如果使用了事務(wù)坞生,從Spring事務(wù)上下文獲茸幸邸)
  2. 調(diào)用SqlSession的接口方法操作數(shù)據(jù)庫獲取結(jié)果
  3. 返回結(jié)果集
  4. 若發(fā)生異常則轉(zhuǎn)換后拋出異常,并最終關(guān)閉SqlSession對象
private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //獲取SqlSession(這個sqlSession才是真正使用的是己,它不是線程安全的)
                //這個方法可以根據(jù)Spring的事務(wù)上下文來獲取事務(wù)范圍內(nèi)的SqlSession
                SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
                //調(diào)用sqlSession對象的方法(select又兵、update等)
        Object result = method.invoke(sqlSession, args);
                //判斷是否為事務(wù)操作,如果未被Spring事務(wù)托管則自動提交commit
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
                //如果出現(xiàn)異常則根據(jù)情況轉(zhuǎn)換后拋出
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
                //最終關(guān)閉sqlSession對象
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

重點(diǎn)分析getSqlSession方法如下:

  1. 若無法從當(dāng)前線程的ThreadLocal中獲取則通過SqlSessionFactory獲取SqlSession
  2. 若開啟了事務(wù),則從當(dāng)前線程的ThrealLocal上下文中獲取SqlSessionHolder
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
        //若開啟了事務(wù)支持沛厨,則從當(dāng)前的ThreadLocal上下文中獲取SqlSessionHolder
        //SqlSessionHolder是SqlSession的包裝類
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
        //若無法從ThrealLocal上下文中獲取則通過SqlSessionFactory獲取SqlSession
    session = sessionFactory.openSession(executorType);
        //若為事務(wù)操作宙地,則注冊SqlSessionHolder到ThrealLocal中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

大致的分析到此為止,文中只對主要的過程進(jìn)行了大致的說明逆皮,小伙伴若想要仔細(xì)分析宅粥,可以自己打開源碼走一遍!

SqlSessionManger又是什么电谣?

SqlSessionManager是Mybatis提供的線程安全的操作類秽梅,且看定義如下:

file

通過上圖可以發(fā)現(xiàn)SqlSessionManager的構(gòu)造方法竟然是private的,那我們怎么創(chuàng)建對象呢剿牺?其實(shí)SqlSessionManager創(chuàng)建對象是通過newInstance方法創(chuàng)建對象的风纠,但需要注入它雖然是私有的構(gòu)造方法,并且提供給我們一個公有的newInstance方法牢贸,但它并不是一個單例模式!

newInstance有很多重載方法镐捧,如下所示:

public static SqlSessionManager newInstance(Reader reader) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
}

public static SqlSessionManager newInstance(Reader reader, String environment) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, environment, null));
}

public static SqlSessionManager newInstance(Reader reader, Properties properties) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, properties));
}

public static SqlSessionManager newInstance(InputStream inputStream) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, null));
}

public static SqlSessionManager newInstance(InputStream inputStream, String environment) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, environment, null));
}

public static SqlSessionManager newInstance(InputStream inputStream, Properties properties) {
  return new SqlSessionManager(new SqlSessionFactoryBuilder().build(inputStream, null, properties));
}

public static SqlSessionManager newInstance(SqlSessionFactory sqlSessionFactory) {
  return new SqlSessionManager(sqlSessionFactory);
}

SqlSessionManager的openSession方法及其重載方法是直接通過調(diào)用底層封裝SqlSessionFactory對象的openSession方法來創(chuàng)建SqlSession對象的潜索,如下所示:

@Override
public SqlSession openSession(boolean autoCommit) {
  return sqlSessionFactory.openSession(autoCommit);
}

@Override
public SqlSession openSession(Connection connection) {
  return sqlSessionFactory.openSession(connection);
}

@Override
public SqlSession openSession(TransactionIsolationLevel level) {
  return sqlSessionFactory.openSession(level);
}

@Override
public SqlSession openSession(ExecutorType execType) {
  return sqlSessionFactory.openSession(execType);
}

@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
  return sqlSessionFactory.openSession(execType, autoCommit);
}

@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
  return sqlSessionFactory.openSession(execType, level);
}

@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
  return sqlSessionFactory.openSession(execType, connection);
}

SqlSessionManager中實(shí)現(xiàn)SqlSession接口中的方法,例如:select懂酱、update等竹习,都是直接調(diào)用SqlSessionProxy代理對象中相應(yīng)的方法,在創(chuàng)建該代理對像的時候使用的InvocationHandler對象是SqlSessionInterceptor列牺,他是定義在SqlSessionManager的一個內(nèi)部類整陌,其定義如下:

private class SqlSessionInterceptor implements InvocationHandler {
  public SqlSessionInterceptor() {
      // Prevent Synthetic Access
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //獲取當(dāng)前ThreadLocal上下文的SqlSession
        final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
    if (sqlSession != null) {
      try {
                //從上下文獲取到SqlSession之后調(diào)用對應(yīng)的方法
        return method.invoke(sqlSession, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    } else {
            //如果無法從ThreadLocal上下文中獲取SqlSession則新建一個SqlSession
      try (SqlSession autoSqlSession = openSession()) {
        try {
          final Object result = method.invoke(autoSqlSession, args);
          autoSqlSession.commit();
          return result;
        } catch (Throwable t) {
          autoSqlSession.rollback();
          throw ExceptionUtil.unwrapThrowable(t);
        }
      }
    }
  }
}

此處我們在思考下ThreadLocal的localSqlSession對象在什么時候賦值對應(yīng)的SqlSession,往上查找最終定位代碼(若調(diào)用startManagerSession方法將設(shè)置ThreadLocal的localSqlSession上下文中的SqlSession對象)瞎领,如下所示:

public void startManagedSession() {
  this.localSqlSession.set(openSession());
}

public void startManagedSession(boolean autoCommit) {
  this.localSqlSession.set(openSession(autoCommit));
}

public void startManagedSession(Connection connection) {
  this.localSqlSession.set(openSession(connection));
}

public void startManagedSession(TransactionIsolationLevel level) {
  this.localSqlSession.set(openSession(level));
}

public void startManagedSession(ExecutorType execType) {
  this.localSqlSession.set(openSession(execType));
}

public void startManagedSession(ExecutorType execType, boolean autoCommit) {
  this.localSqlSession.set(openSession(execType, autoCommit));
}

public void startManagedSession(ExecutorType execType, TransactionIsolationLevel level) {
  this.localSqlSession.set(openSession(execType, level));
}

public void startManagedSession(ExecutorType execType, Connection connection) {
  this.localSqlSession.set(openSession(execType, connection));
}

SqlSessionTemplate與SqlSessionManager的聯(lián)系與區(qū)別

  • SqlSessionTemplate是Mybatis為了接入Spring提供的Bean泌辫。通過TransactionSynchronizationManager中的ThreadLocal<Map<Object, Object>>保存線程對應(yīng)的SqlSession,實(shí)現(xiàn)session的線程安全九默。
  • SqlSessionManager是Mybatis不接入Spring時用于管理SqlSession的Bean震放。通過SqlSessionManagger的ThreadLocal<SqlSession>實(shí)現(xiàn)session的線程安全。

總結(jié)分析

通過上面的代碼分析驼修,我們可以看出Spring解決SqlSession線程安全問題的思路就是動態(tài)代理與ThreadLocal的運(yùn)用殿遂,我們可以觸類旁通:當(dāng)遇到線程不安全的類,但是又想當(dāng)作線程安全的類使用乙各,則可以使用ThreadLocal進(jìn)行線程上下文的隔離墨礁,此處的動態(tài)代理技術(shù)更好的解決了上層API調(diào)用的非侵入性,保證API接口調(diào)用的高內(nèi)聚耳峦、低耦合原則

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布恩静!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蹲坷,隨后出現(xiàn)的幾起案子蜕企,更是在濱河造成了極大的恐慌咬荷,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轻掩,死亡現(xiàn)場離奇詭異幸乒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)唇牧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門罕扎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丐重,你說我怎么就攤上這事腔召。” “怎么了扮惦?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵臀蛛,是天一觀的道長。 經(jīng)常有香客問我崖蜜,道長浊仆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任豫领,我火速辦了婚禮抡柿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘等恐。我一直安慰自己洲劣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布课蔬。 她就那樣靜靜地躺著囱稽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪二跋。 梳的紋絲不亂的頭發(fā)上粗悯,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音同欠,去河邊找鬼样傍。 笑死,一個胖子當(dāng)著我的面吹牛铺遂,可吹牛的內(nèi)容都是我干的衫哥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼襟锐,長吁一口氣:“原來是場噩夢啊……” “哼撤逢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蚊荣,失蹤者是張志新(化名)和其女友劉穎初狰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體互例,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奢入,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了媳叨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腥光。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糊秆,靈堂內(nèi)的尸體忽然破棺而出武福,到底是詐尸還是另有隱情,我是刑警寧澤痘番,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布捉片,位于F島的核電站,受9級特大地震影響汞舱,放射性物質(zhì)發(fā)生泄漏伍纫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一兵拢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逾礁,春花似錦说铃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砾嫉,卻和暖如春幼苛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焕刮。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工舶沿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人配并。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓括荡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親溉旋。 傳聞我的和親對象是個殘疾皇子畸冲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容