mybatis源碼解析八(spring處理sqlsession線程安全問題)

上一期,分析了下關(guān)于mybatis的處理sqlsession線程安全的問題,主要是通過sqlSessionManager代理類增強(qiáng)的形式,通過每次創(chuàng)建一個(gè)新的DefautSqlsession或者將當(dāng)前線程放入到Threadlocal中實(shí)現(xiàn)的,那么我們?cè)谑褂胢ybatis的時(shí)候,一般不可能單獨(dú)使用mybatis的,一般都是和sprig框架配合使用,現(xiàn)在都是面向spring編程了,所以,本次我們一起分析下spring是怎樣保證sqlSession線程安全的,
我們先通過一個(gè)案例,看看結(jié)果,一般在引入spring后,我們都會(huì)通過SqlSessionTemplete操作mybatis,當(dāng)掃描到@Mapper的注解后,在執(zhí)行業(yè)務(wù)邏輯代碼,執(zhí)行數(shù)據(jù)庫交互的時(shí)候,這個(gè)時(shí)候,就會(huì)被SqlSessionTemplete攔截了,所以,不管是我們顯示的調(diào)用SqlSessionTemplete執(zhí)行相關(guān)的操作,還是在引入spring框架后,,默認(rèn)的執(zhí)行,都是通過SqlSessionTempete攔截保證sqlSqssion線程安全的,那馬接下來,我們就一起分析下,看看spring到底是怎樣支持sqlSession線程安全的
我們先通過一個(gè)案例來說明問題,下面是一個(gè)特備簡單的案例,我們通過他,先來分析下,然后在通過多線程來分析具體的線程安全問題

@Test
    public void test5(){
        final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey);
        final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey1);

    }

當(dāng)我們執(zhí)行 ryxAccountService.getRyxAccountByPrimaryKey的接口方法的時(shí)候,到mapper接口,SqlSqlSessionTemplete通過代理的方式攔截mapper接口,生成代理類,獲取sqlSession,執(zhí)行后續(xù)的數(shù)據(jù)庫crud

public class SqlSessionTemplate implements SqlSession, DisposableBean {

    private final SqlSessionFactory sqlSessionFactory;

    private final ExecutorType executorType;

    private final SqlSession sqlSessionProxy;

    private final PersistenceExceptionTranslator exceptionTranslator;

         省略........

    /**
     * Constructs a Spring managed {@code SqlSession} with the given
     * {@code SqlSessionFactory} and {@code ExecutorType}.
     * A custom {@code SQLExceptionTranslator} can be provided as an
     * argument so any {@code PersistenceException} thrown by MyBatis
     * can be custom translated to a {@code RuntimeException}
     * The {@code SQLExceptionTranslator} can also be null and thus no
     * exception translation will be done and MyBatis exceptions will be
     * thrown
     *
     * @param sqlSessionFactory a factory of SqlSession
     * @param executorType an executor type on session
     * @param exceptionTranslator a translator of exception
     */
    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;
                //jdk動(dòng)態(tài)代理方法
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
                                //獲取類加載器
                SqlSessionFactory.class.getClassLoader(),
                                //具體需要代理的類
                new Class[] { SqlSession.class },
                                //執(zhí)行增強(qiáng)的方法
                new SqlSessionInterceptor());
    }

    省略......

    /**
     * Proxy needed to route MyBatis method calls to the proper SqlSession got
     * from Spring's Transaction Manager
     * It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
     * pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
     */
    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = getSqlSession(
                    SqlSessionTemplate.this.sqlSessionFactory,
                    SqlSessionTemplate.this.executorType,
                    SqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args);
                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) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }
            }
        }
    }

}

看到這段代碼是不是有似曾相識(shí)的感覺,沒錯(cuò),前一期在介紹SqlSesionManager的時(shí)候,也是這個(gè)寫法,通過動(dòng)態(tài)代理的技術(shù)增強(qiáng)目標(biāo)方法,這里的作用就是在生成目標(biāo)類之前,獲取sqlSession(線程安全的),接下來我們吧重心放到SqlSessionInterceptor 的getSqlSession中去

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
        //參數(shù)校驗(yàn)
        notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
        notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
        
        /**
         * 1.1
         * 這個(gè)步驟都是指的是存在事務(wù)的情況下的結(jié)果,也就是說,在你的方法上開啟了事務(wù)注解的時(shí)候,才有意義,當(dāng)不開啟事務(wù)注解的時(shí)候,
         * 會(huì)直接調(diào)用openSession返回session,也就是說,不存在事務(wù)的情況下,每一個(gè)線程都是相當(dāng)于開啟一個(gè)新的session,也就不會(huì)存在一級(jí)緩存的問題
         * 當(dāng)然也就不會(huì)存在線程安全問題
         * 調(diào)用事務(wù)同步管理器方法獲取SqlSession回話持有者,回去ThreadLocal中去取會(huì)話持有者對(duì)象.會(huì)話持有者對(duì)象
         * 放在ThreadLocal中.ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
         */
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

        //會(huì)話持有者的數(shù)量執(zhí)行+1操作,有人想獲取持有者資源,將引用計(jì)數(shù)器加1
        SqlSession session = sessionHolder(executorType, holder);
        if (session != null) {
            return session;
        }

        //如果回話持有者中不存在SqlSession.則調(diào)用sessionFactory開啟一個(gè)session
        LOGGER.debug(() -> "Creating a new SqlSession");
        session = sessionFactory.openSession(executorType);

        /**
         * 1.2
         * 注冊(cè)到會(huì)話持有者中,如果存在事務(wù),就會(huì)將當(dāng)前的會(huì)話持有者和sessionFactory添加到thredLocal中
         */
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

        return session;
    }

這節(jié)我們的重點(diǎn)是分析SqlSession.所以事物我們暫且放下,看懂了,代碼比較簡單.我這里在總結(jié)下
1:當(dāng)我們引入spring框架后,我們執(zhí)行查詢,SqLSessionTemplete戶通過代理方式"攔截"執(zhí)行方法,由于DefaultSqSLession線程不安全
2:這里獲取線程安全的SqlSession,
3:判斷是否存在事務(wù)
4:事務(wù)不存在,直接調(diào)用SqlSessionFactory獲取sesison返回,直接執(zhí)行后續(xù)的方法,也就是說,不通的線程訪問,或者同一個(gè)線程范根同樣的查詢方法,都會(huì)去創(chuàng)建一個(gè)新的session,這樣的話,也就導(dǎo)致以及緩存不存在了.
如果存在事務(wù),回去事務(wù)資源管理器獲取session持有者,如果存在,就將當(dāng)前的引用計(jì)數(shù)加1,如果不存在,則通過SqlSessionFactory.openSession獲取一個(gè)session,然后注冊(cè)到事務(wù)管理器中,這里是將當(dāng)前資源添加到ThreadLOcal中,,在local中定義了一個(gè)map類似于這樣的結(jié)構(gòu)ThreadLocal<Map<String,Object>>,
5:調(diào)用DefaultSqLSession執(zhí)行后續(xù)的邏輯,所以也就是說,當(dāng)存在事物的情況下,一級(jí)緩存才有意義

我們通過案例來加強(qiáng)一下理解,以下案例,查詢兩次,默認(rèn)回去走SqLSessionTemplete,我們執(zhí)行下,看看結(jié)果

@Test
    public void test5(){
        final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey);
        final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey1);

    }

我們看到結(jié)果中查詢兩次都走了數(shù)據(jù)庫查詢,一級(jí)緩存沒有起作用


image.png

再來看另一個(gè)案例,加了注解后,第二次查詢回去走緩存

     */
    @Test
    @Transactional
    public void test5(){
        final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey);
        final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
        System.out.println(ryxAccountByPrimaryKey1);

    }

image.png

那就有一個(gè)問題,為什么mybatis不適用線程安全額SqlSessionManager那,而是默認(rèn)使用線程不安全的DefaultSqlSession那
我覺得這個(gè)問題的答案是
DefaultSqlSession已經(jīng)開發(fā)完了,那馬在他的基礎(chǔ)上修改的話,勢(shì)必要考慮的東西比較多,還不如直接通過代理的方式增強(qiáng)這個(gè)方法,調(diào)用底層的DefauleSqLSession,如果再來個(gè)框架,整合mybatis,又要修改這一塊,導(dǎo)致越來越臃腫,通過代理的方式不修改原來代碼的基礎(chǔ)上,實(shí)現(xiàn)了該功能,其實(shí)是很值得我們借鑒的,做到了解耦操作.
文中要是有不合理的地方,還請(qǐng)包涵指正,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末串结,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歪今,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躬存,死亡現(xiàn)場(chǎng)離奇詭異谋作,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)列吼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門幽崩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寞钥,你說我怎么就攤上這事慌申。” “怎么了理郑?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵蹄溉,是天一觀的道長。 經(jīng)常有香客問我您炉,道長柒爵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任赚爵,我火速辦了婚禮棉胀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冀膝。我一直安慰自己唁奢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布窝剖。 她就那樣靜靜地躺著麻掸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赐纱。 梳的紋絲不亂的頭發(fā)上论笔,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天采郎,我揣著相機(jī)與錄音,去河邊找鬼狂魔。 笑死蒜埋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的最楷。 我是一名探鬼主播整份,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼籽孙!你這毒婦竟也來了烈评?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤犯建,失蹤者是張志新(化名)和其女友劉穎讲冠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體适瓦,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竿开,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年琉闪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虱咧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡儿惫,死狀恐怖嗦随,靈堂內(nèi)的尸體忽然破棺而出列荔,到底是詐尸還是另有隱情,我是刑警寧澤枚尼,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布贴浙,位于F島的核電站,受9級(jí)特大地震影響署恍,放射性物質(zhì)發(fā)生泄漏崎溃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一锭汛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袭蝗,春花似錦唤殴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乡范,卻和暖如春配名,著一層夾襖步出監(jiān)牢的瞬間啤咽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國打工渠脉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宇整,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓芋膘,卻偏偏與公主長得像鳞青,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子为朋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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