Mybatis 源碼分析(三)之 Mybatis 的一級緩存和二級緩存

2.jpg

Mybatis 源碼分析(三)之 Mybatis 的一級緩存和二級緩存

Mybatis系列:
Mybatis 基礎介紹與逆向工程的構建 :http://www.lxweimin.com/p/1c18db4d7a38
Mybatis 源碼分析(一)之 Mybatis 的Executor的初始化:http://www.lxweimin.com/p/c7425c841337
Mybatis 源碼分析(二)之 Mybatis 操作數據庫的流程 :http://www.lxweimin.com/p/11d354ec3612
Mybatis 源碼分析(三)之 Mybatis 的一級緩存和二級緩存 :http://www.lxweimin.com/p/5515640d14fe


Mybatis緩存的作用

每當我們使用 MyBatis 開啟一次和數據庫的會話,MyBatis 會創建出一個 SqlSession 對象表示一次數據庫會話。

在對數據庫的一次會話中,我們有可能會反復地執行完全相同的查詢語句,如果不采取一些措施的話,每一次查詢都會查詢一次數據庫,而我們在極短的時間內做了完全相同的查詢,那么它們的結果極有可能完全相同,由于查詢一次數據庫的代價很大,這有可能造成很大的資源浪費。

為了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不需要再進行一次數據庫查詢了。

mybatis的緩存有一級緩存和二級緩存。

一級緩存

一級緩存是默認開啟的,作用域是session級別的,緩存的key格式如下:

cache key: id + sql + limit + offset 

在commit之前,第一次查詢結果換以key value的形式存起來,如果有相同的key進來,直接返回value,這樣有助于減輕數據的壓力。

相關源碼:

org.apache.ibatis.executor.BaseExecutor#createCacheKey

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(rowBounds.getOffset());
        cacheKey.update(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);
            }
        }

        if (this.configuration.getEnvironment() != null) {
            cacheKey.update(this.configuration.getEnvironment().getId());
        }

        return cacheKey;
    }
}

查詢數據庫并存入一級緩存的語句

org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

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);
    }

    //將查詢出來的結果存入一級緩存
    this.localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
     //如果是存儲過程把參數存入localOutputParameterCache
        this.localOutputParameterCache.putObject(key, parameter);
    }

    return list;
}

并且當commit或者rollback的時候會清除緩存,并且當執行insert、update、delete的時候也會清除緩存。

相關源碼:

org.apache.ibatis.executor.BaseExecutor#update

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        //刪除一級緩存
        this.clearLocalCache();
        return this.doUpdate(ms, parameter);
    }
}


 org.apache.ibatis.executor.BaseExecutor#commit
 
 public void commit(boolean required) throws SQLException {
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
        //刪除一級緩存
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
            this.transaction.commit();
        }

    }
}

org.apache.ibatis.executor.BaseExecutor#rollback

public void rollback(boolean required) throws SQLException {
    if (!this.closed) {
        try {
            //刪除一級緩存
            this.clearLocalCache();
            this.flushStatements(true);
        } finally {
            if (required) {
                this.transaction.rollback();
            }

        }
    }

}
二級緩存

二級緩存是手動開啟的,作用域為sessionfactory(也可以說MapperStatement級緩存,也就是一個namespace就會有一個緩存),因為二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的,也就是要求實現Serializable接口,如果存儲在內存中的話,實測不序列化也可以的。

如果開啟了二級緩存的話,你的Executor將會被裝飾成CachingExecutor,緩存是通過CachingExecutor來操作的,查詢出來的結果會存在statement中的cache中,若有更新,刪除類的操作默認就會清空該MapperStatement的cache(也可以通過修改xml中的屬性,讓它不執行),不會影響其他的MapperStatement。

相關源碼:

org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? this.defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Object executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }

    //是否開啟緩存,傳入的參數為SimpleExecutor
    if (this.cacheEnabled) {
        executor = new CachingExecutor((Executor)executor);
    }

    //責任鏈模式攔截器
    Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
    return executor;
}

query

org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //獲得該MappedStatement的cache
    Cache cache = ms.getCache();
    //如果緩存不為空
    if (cache != null) {
        //看是否需要清除cache(在xml中可以配置flushCache屬性決定何時清空cache)
        this.flushCacheIfRequired(ms);
        //若開啟了cache且resultHandler 為空
        if (ms.isUseCache() && resultHandler == null) {
            this.ensureNoOutParams(ms, parameterObject, boundSql);
            //從TransactionalCacheManager中取cache
            List<E> list = (List)this.tcm.getObject(cache, key);
            //若取出來list是空的
            if (list == null) {
                //查詢數據庫
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                //將結果存入cache中
                this.tcm.putObject(cache, key, list);
            }

            return list;
        }
    }
    //如果緩存為空,去查詢數據庫
    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

對于

this.tcm.getObject(cache, key);  

因同一個namespace下的MappedStatement的cache是同一個,而TransactionalCacheManager中統一管理cache是里面的屬性transactionalCaches,該屬性以MappedStatement中的Cache為key,TransactionalCache對象為Value。即一個namespace對應一個TransactionalCache。

相關源碼:

TransactionalCacheManager

org.apache.ibatis.cache.TransactionalCacheManager

public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap();

...
}

TransactionalCache

org.apache.ibatis.cache.decorators.TransactionalCache

public class TransactionalCache implements Cache {
    private static final Log log = LogFactory.getLog(TransactionalCache.class);
    //namespace中的cache
    private Cache delegate;
    //提交的時候清除cache的標志位
    private boolean clearOnCommit;
    //待提交的集合
    private Map<Object, Object> entriesToAddOnCommit;
    //未查到的key存放的集合
    private Set<Object> entriesMissedInCache;

...
}

update

//更新
org.apache.ibatis.executor.CachingExecutor#update

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
     //看是否需要清除cache(在xml中可以配置flushCache屬性決定何時清空cache)
    this.flushCacheIfRequired(ms);
    return this.delegate.update(ms, parameterObject);
}

org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired

private void flushCacheIfRequired(MappedStatement ms) {
    //獲得cache
    Cache cache = ms.getCache();
    //若isFlushCacheRequired為true,則清除cache
    if (cache != null && ms.isFlushCacheRequired()) {
        this.tcm.clear(cache);
    }

}
一級、二級緩存測試

因為一級緩存是默認生效的,下面是二級緩存開啟步驟。

mybatis-config.xml

<settings>
    <!--這個配置使全局的映射器(二級緩存)啟用或禁用緩存-->
    <setting name="cacheEnabled" value="true" />
</settings>

在mapper.xml可以進行如下的配置

<mapper>
   <!--開啟本mapper的namespace下的二級緩存-->
    <!--
        eviction:代表的是緩存回收策略,目前MyBatis提供以下策略。
        (1) LRU,最近最少使用的,一處最長時間不用的對象
        (2) FIFO,先進先出,按對象進入緩存的順序來移除他們
        (3) SOFT,軟引用,移除基于垃圾回收器狀態和軟引用規則的對象
        (4) WEAK,弱引用,更積極的移除基于垃圾收集器狀態和弱引用規則的對象。這里采用的是LRU,
                移除最長時間不用的對形象
  
        flushInterval:刷新間隔時間,單位為毫秒,這里配置的是100秒刷新,如果你不配置它,那么當
        SQL被執行的時候才會去刷新緩存。
  
        size:引用數目,一個正整數,代表緩存最多可以存儲多少個對象,不宜設置過大。設置過大會導致內存溢出。
        這里配置的是1024個對象
  
        readOnly:只讀,意味著緩存數據只能讀取而不能修改,這樣設置的好處是我們可以快速讀取緩存,缺點是我們沒有
        辦法修改緩存,他的默認值是false,不允許我們修改
    -->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>

    <!--刷新二級緩存-->
  <update id="updateByPrimaryKey" parameterType="com.demo.mybatis.pojo.User" flushCache="true">
    update user
    set name = #{name,jdbcType=VARCHAR},
      age = #{age,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>

   <!--可以通過設置useCache來規定這個sql是否開啟緩存,ture是開啟,false是關閉-->
   <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap"   useCache="true" >
     select
     <include refid="Base_Column_List" />
     from user
     where id = #{id,jdbcType=INTEGER}
   </select>
  
</mapper>

其中僅僅添加下面這個也可以

 <cache/>

如果我們配置了二級緩存就意味著:

  • 映射語句文件中的所有select語句將會被緩存。

  • 映射語句文件中的所欲insert、update和delete語句會刷新緩存。

  • 緩存會使用默認的Least Recently Used(LRU,最近最少使用的)算法來收回。

  • 根據時間表,比如No Flush Interval,(CNFI沒有刷新間隔),緩存不會以任何時間順序來刷新。

  • 緩存會存儲列表集合或對象(無論查詢方法返回什么)的1024個引用。

  • 緩存會被視為是read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而且可以安全的被調用者修改,不干擾其他調用者或線程所做的潛在修改。

User.java

public class User implements Serializable {
    private Integer id;

    private String name;

    private Integer age;

    private static final long serialVersionUID = 1L;
    
    ...
    set/get
    ...
    
}

UserMapper.java

    User selectByPrimaryKey(Integer id);

測試方法

@Test
public void test03() throws IOException {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    //第一次
    User user = userMapper.selectByPrimaryKey(1);
    System.out.println("user1 => " + user.toString());
    //第二次
    User user2 = userMapper.selectByPrimaryKey(1);
    System.out.println("user2 => " + user2.toString());
    //session提交
    sqlSession.commit();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    //第三次
    User user3 = userMapper2.selectByPrimaryKey(1);
    System.out.println("user3 => " + user3.toString());
    //第四次
    User user4 = userMapper2.selectByPrimaryKey(1);
    System.out.println("user4 => " + user4.toString());
    sqlSession2.commit();
}

來看下結果

DEBUG 2019-01-30 00:01:29791 Opening JDBC Connection
DEBUG 2019-01-30 00:01:34688 Created connection 1121453612.
DEBUG 2019-01-30 00:01:34689 Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@42d8062c]
DEBUG 2019-01-30 00:01:34691 ==>  Preparing: select id, name, age from user where id = ? 
DEBUG 2019-01-30 00:01:34737 ==> Parameters: 1(Integer)
DEBUG 2019-01-30 00:01:34757 <==      Total: 1
user1 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34757 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.0
user2 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34818 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.3333333333333333
user3 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34819 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.5
user4 => User{id=1, name='ayang', age=18}

可以看到第一次和第二次走的是一級緩存,第三次和第四次走的是二級緩存。

總結:

一級緩存是自動開啟的,sqlSession級別的緩存,查詢結果存放在BaseExecutor中的localCache中。

如果第一次做完查詢,接著做一次update | insert | delete | commit | rollback操作,則會清除緩存,第二次查詢則繼續走數據庫。

對于一級緩存不同的sqlSession之間的緩存是互相不影響的。

二級緩存是手動開啟的,作用域為sessionfactory,也可以說MapperStatement級緩存,也就是一個namespace(mapper.xml)就會有一個緩存,不同的sqlSession之間的緩存是共享的。

因為二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的,也就是要求實現Serializable接口,如果存儲在內存中的話,實測不序列化也可以的。

一般為了避免出現臟數據,所以我們可以在每一次的insert | update | delete操作后都進行緩存刷新,也就是在Statement配置中配置flushCache屬性,如下:

<!--刷新二級緩存  flushCache="true"-->
  <update id="updateByPrimaryKey" parameterType="com.demo.mybatis.pojo.User" flushCache="true">
    update user
    set name = #{name,jdbcType=VARCHAR},
      age = #{age,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
</update>

如有問題歡迎留言:)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容