Mybatis原理--緩存機制(一級緩存)

MyBatis將數(shù)據(jù)緩存設(shè)計成兩級結(jié)構(gòu),分為一級緩存酝碳、二級緩存:
一級緩存是Session會話級別的緩存:表示一次數(shù)據(jù)庫會話的SqlSession對象之中伴网,又被稱之為本地緩存逃沿。一級緩存是MyBatis內(nèi)部實現(xiàn)的一個特性透揣,用戶不能配置济炎,默認(rèn)情況下自動支持的緩存,用戶沒有定制它的權(quán)利(不過這也不是絕對的辐真,可以通過開發(fā)插件對它進(jìn)行修改)须尚;
二級緩存是Application應(yīng)用級別的緩存:它的是生命周期很長,跟Application的聲明周期一樣侍咱,也就是說它的作用范圍是整個Application應(yīng)用耐床。

由于MyBatis使用SqlSession對象表示一次數(shù)據(jù)庫的會話,為了減少資源的浪費楔脯,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存撩轰,將每次查詢到的結(jié)果結(jié)果緩存起來,當(dāng)下次查詢的時候,如果判斷先前有個完全一樣的查詢堪嫂,會直接從緩存中直接將結(jié)果取出偎箫,返回給用戶,不需要再進(jìn)行一次數(shù)據(jù)庫查詢了溉苛。

一級緩存

實際上, SqlSession只是一個MyBatis對外的接口镜廉,SqlSession將它的工作交給了Executor執(zhí)行器這個角色來完成弄诲,負(fù)責(zé)完成對數(shù)據(jù)庫的各種操作愚战。當(dāng)創(chuàng)建了一個SqlSession對象時,MyBatis會為這個SqlSession對象創(chuàng)建一個新的Executor執(zhí)行器齐遵,而緩存信息就被維護在這個Executor執(zhí)行器中寂玲,MyBatis將緩存和對緩存相關(guān)的操作封裝成了Cache接口中,
Executor接口的實現(xiàn)類BaseExecutor中擁有一個Cache接口的實現(xiàn)類PerpetualCache梗摇,則對于BaseExecutor對象而言拓哟,它將使用PerpetualCache對象維護緩存

public class PerpetualCache implements Cache {
    private String id;
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public boolean equals(Object o) {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if(this == o) {
            return true;
        } else if(!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}
一級緩存的生命周期
  1. MyBatis在開啟一個數(shù)據(jù)庫會話時,會 創(chuàng)建一個新的SqlSession對象伶授,SqlSession對象中會有一個新的Executor對象断序,Executor對象中持有一個新的PerpetualCache對象;當(dāng)會話結(jié)束時糜烹,SqlSession對象及其內(nèi)部的Executor對象還有PerpetualCache對象也一并釋放掉违诗。
  2. 如果SqlSession調(diào)用了close()方法,會釋放掉一級緩存PerpetualCache對象疮蹦,一級緩存將不可用诸迟;
  3. 如果SqlSession調(diào)用了clearCache(),會清空PerpetualCache對象中的數(shù)據(jù)愕乎,但是該對象仍可使用阵苇;
    4.SqlSession中執(zhí)行了任何一個update操作(update()、delete()感论、insert())绅项,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用比肄;
一級緩存的實現(xiàn)
  • 對于某個查詢快耿,根據(jù)statementId,params,rowBounds來構(gòu)建一個key值,根據(jù)這個key值去緩存Cache中取出對應(yīng)的key值存儲的緩存結(jié)果薪前;
  • 判斷從Cache中根據(jù)特定的key值取的數(shù)據(jù)數(shù)據(jù)是否為空润努,即是否命中;
  • 如果命中示括,則直接將緩存結(jié)果返回铺浇;
  • 如果沒命中:
    1. 去數(shù)據(jù)庫中查詢數(shù)據(jù),得到查詢結(jié)果垛膝;
    2. 將key和查詢到的結(jié)果分別作為key,value對存儲到Cache中鳍侣;
    3. 將查詢結(jié)果返回丁稀;
  • 結(jié)束。
Cache接口的設(shè)計以及CacheKey的定義
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        //構(gòu)建緩存需要的key值
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

//構(gòu)建緩存需要的key值
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
            cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
            cacheKey.update(boundSql.getSql());
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if(boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if(parameterObject == null) {
                        value = null;
                    } else if(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            return cacheKey;
        }
    }

調(diào)用如下方法時倚聚,debug源碼的截圖
//mapper接口的方法 schoolCustomerDao.selectBySome(1l, "2017-09-17","120706049");

Debug調(diào)試截圖

CacheKey構(gòu)建好了之后线衫,就可以存儲查詢后的結(jié)果了,源碼如下所示:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }
        //緩存查詢的結(jié)果
        this.localCache.putObject(key, list);
        if(ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }

得出CacheKey由以下條件可以得到:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數(shù)值

一級緩存的性能分析

我將從兩個 一級緩存的特性來討論SqlSession的一級緩存性能問題:

  • MyBatis對會話(Session)級別的一級緩存設(shè)計的比較簡單惑折,就簡單地使用了HashMap來維護授账,并沒有對HashMap的容量和大小進(jìn)行限制。
    讀者有可能就覺得不妥了:如果我一直使用某一個SqlSession對象查詢數(shù)據(jù)惨驶,這樣會不會導(dǎo)致HashMap太大白热,而導(dǎo)致 java.lang.OutOfMemoryError錯誤啊粗卜? 讀者這么考慮也不無道理屋确,不過MyBatis的確是這樣設(shè)計的。
    MyBatis這樣設(shè)計也有它自己的理由:
  1. 一般而言SqlSession的生存時間很短续扔。一般情況下使用一個SqlSession對象執(zhí)行的操作不會太多攻臀,執(zhí)行完就會消亡;
  2. 對于某一個SqlSession對象而言纱昧,只要執(zhí)行update操作(update刨啸、insert、delete)砌些,都會將這個SqlSession對象中對應(yīng)的一級緩存清空掉呜投,所以一般情況下不會出現(xiàn)緩存過大,影響JVM內(nèi)存空間的問題存璃;
  3. 可以手動地釋放掉SqlSession對象中的緩存仑荐。
  • 一級緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念
    MyBatis的一級緩存就是使用了簡單的HashMap纵东,MyBatis只負(fù)責(zé)將查詢數(shù)據(jù)庫的結(jié)果存儲到緩存中去粘招, 不會去判斷緩存存放的時間是否過長、是否過期偎球,因此也就沒有對緩存的結(jié)果進(jìn)行更新這一說了洒扎。

根據(jù)一級緩存的特性,在使用的過程中衰絮,我認(rèn)為應(yīng)該注意:

  1. 對于數(shù)據(jù)變化頻率很大袍冷,并且需要高時效準(zhǔn)確性的數(shù)據(jù)要求,我們使用SqlSession查詢的時候猫牡,要控制好SqlSession的生存時間胡诗,SqlSession的生存時間越長,它其中緩存的數(shù)據(jù)有可能就越舊,從而造成和真實數(shù)據(jù)庫的誤差煌恢;同時對于這種情況骇陈,用戶也可以手動地適時清空SqlSession中的緩存;
  2. 對于只執(zhí)行瑰抵、并且頻繁執(zhí)行大范圍的select操作的SqlSession對象你雌,SqlSession對象的生存時間不應(yīng)過長。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末二汛,一起剝皮案震驚了整個濱河市婿崭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌习贫,老刑警劉巖逛球,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異苫昌,居然都是意外死亡,警方通過查閱死者的電腦和手機幸海,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門祟身,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人物独,你說我怎么就攤上這事袜硫。” “怎么了挡篓?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵婉陷,是天一觀的道長。 經(jīng)常有香客問我官研,道長秽澳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任戏羽,我火速辦了婚禮担神,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘始花。我一直安慰自己妄讯,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布酷宵。 她就那樣靜靜地躺著亥贸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浇垦。 梳的紋絲不亂的頭發(fā)上炕置,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音,去河邊找鬼讹俊。 笑死垦沉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的仍劈。 我是一名探鬼主播厕倍,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贩疙!你這毒婦竟也來了讹弯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤这溅,失蹤者是張志新(化名)和其女友劉穎组民,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悲靴,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡臭胜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了癞尚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耸三。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浇揩,靈堂內(nèi)的尸體忽然破棺而出仪壮,到底是詐尸還是另有隱情,我是刑警寧澤胳徽,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布积锅,位于F島的核電站,受9級特大地震影響养盗,放射性物質(zhì)發(fā)生泄漏缚陷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一爪瓜、第九天 我趴在偏房一處隱蔽的房頂上張望蹬跃。 院中可真熱鬧,春花似錦铆铆、人聲如沸蝶缀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翁都。三九已至,卻和暖如春谅猾,著一層夾襖步出監(jiān)牢的瞬間柄慰,已是汗流浹背鳍悠。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坐搔,地道東北人藏研。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像概行,于是被迫代替她去往敵國和親蠢挡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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