MyBatis 的一級緩存與二級緩存

一、簡述

MyBatis 的一級緩存是基于數(shù)據(jù)庫會話(SqlSession 對象)的,默認開啟。二級緩存是基于全局(nameSpace)的,開啟需要配置。

對于 JDBC 操作,如果需要連續(xù)請求 id=1 的用戶數(shù)據(jù),那么就要進行兩次的數(shù)據(jù)庫連接,獲取數(shù)據(jù)庫中的數(shù)據(jù)。相同的數(shù)據(jù),進行兩次數(shù)據(jù)庫連接,這肯定會造成資源的浪費?;诿嫦?qū)ο?,可以把第一次獲取的數(shù)據(jù)保存到一個對象中,下一次直接從對象中獲取就行了,如圖:

獲取的內(nèi)容保存在對象中,在一個請求期間,直接使用或者傳遞對象就可以了。對于 JDBC 的操作,可以自己定義類或者集合來保存數(shù)據(jù)庫中的數(shù)據(jù),來避免連續(xù)請求數(shù)據(jù)庫的問題。這里用來保存數(shù)據(jù)的對象或者集合,也能稱之為緩存。

但是使用了三層架構(gòu)之后,Dao 層和 Dao 層之間有可能互相是不清楚的。如果有一個復雜的業(yè)務要在 Service 層中進行處理,需要分別調(diào)用不同 Dao 層中的數(shù)據(jù),那這樣簡單的緩存還是不夠看。

此時,要再去處理緩存問題,就會花費過多的精力,得不償失。在這種層面上的緩存處理 MyBatis 框架已經(jīng)做好了,就叫做一級緩存。

二、MyBatis 的主要層次結(jié)構(gòu)

使用 MyBatis 對數(shù)據(jù)庫操作的代碼,能夠看見的就是這個 SqlSession 對象。實際上,這只是 MyBatis 對外暴露的接口,整個 MyBatis 核心部件是下面的這么一堆接口和類:

1??SqlSession:MyBatis 工作的主要頂層 API,表示和數(shù)據(jù)庫交互的會話,完成必要數(shù)據(jù)庫增刪改查功能。
2??Executor:MyBatis 執(zhí)行器,整個 MyBatis 調(diào)度的核心,負責 QL 語句的生成和查詢緩存的維護。
3??StatementHandler:封裝了 JDBC Statement 操作,負責對 JDBC statement 的操作,如設(shè)置參數(shù)、將 Statement 結(jié)果集轉(zhuǎn)換成 List 集合。
4??ParameterHandler:負責對用戶傳遞的參數(shù)轉(zhuǎn)換成 JDBC Statement 所需要的參數(shù)。
5??ResultSetHandler:負責將 JDBC 返回的 ResultSet 結(jié)果集對象轉(zhuǎn)換成 List 類型的集合。
6??TypeHandler:負責 Java 數(shù)據(jù)類型和 jdbc 數(shù)據(jù)類型之間的映射和轉(zhuǎn)換。
7??MappedStatement:MappedStatement 維護了一條節(jié)點的封裝。
8??SqlSource:負責根據(jù)用戶傳遞的 parameterObject,動態(tài)地生成 SQL 語句,將信息封裝到 BoundSql 對象中,并返回。
9??BoundSql:表示動態(tài)生成的 SQL 語句以及相應的參數(shù)信息。
1??0??Configuration:MyBatis 所有的配置信息都維持在 Configuration 對象之中。

上面這堆接口和類的層次關(guān)系如圖:

MyBatis 對外暴露的接口是 SqlSession,而最重要的是 Executor 接口。Executor 的實現(xiàn)類 BaseExecutor 中擁有一個 Cache 接口的實現(xiàn)類 PerpetualCache:

PerpetualCache 中則有一個 HashMap 屬性:

總結(jié):

MyBatis 封裝了 JDBC 操作,對外暴露了 SqlSession 接口進行數(shù)據(jù)庫的操作。但是實際 MyBatis 最核心的接口是 Executor,它負責 SQL 語句的生成和查詢緩存的維護。如果沒有緩存就查數(shù)據(jù)庫,有緩存就使用的是 PerpetualCache 中的 HashMap 保存的數(shù)據(jù)緩存。MyBatis 的一級緩存其實就保存在一個 HashMap 中。HashMap 如何判斷查詢方法是否相同?其實主要是通過 HashMap 的 key 值。

BaseExecutor:

...

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();
            Iterator var8 = parameterMappings.iterator();
            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                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;
        }
    }
...

從代碼中可以看出,如果下面條件一樣,就可以判斷為兩個查詢相同:

1??statementId
2??RowBounds 的 offset、limit 的結(jié)果集分頁屬性
3??SQL 語句
4??傳給 JDBC 的參數(shù)值

三、MyBatis 的一級緩存

1??一級緩存最簡單的組織形式

MyBatis 在一次會話的表示——一個 SqlSession 對象中創(chuàng)建一個本地緩存(local cache),對于每一次查詢,都會嘗試根據(jù)查詢的條件去本地緩存中查找是否在緩存中,如果在,就直接從緩存中取出,然后返回給用戶;否則,從數(shù)據(jù)庫讀取數(shù)據(jù),將查詢結(jié)果存入緩存并返回給用戶。

類似于最開始保存的方式,只是從一個簡單的對象,換成了封裝好了的更加復雜的 Local Cache 對象。

實際上,SqlSession 只是一個 MyBatis 對外的接口,SqlSession 將它的工作交給了 Executor 執(zhí)行器這個角色來完成,負責完成對數(shù)據(jù)庫的各種操作。當創(chuàng)建了一個 SqlSession 對象時,MyBatis 會為這個 SqlSession 對象創(chuàng)建一個新的 Executor 執(zhí)行器,而緩存信息就被維護在這個 Executor 執(zhí)行器中,MyBatis 將緩存和對緩存相關(guān)的操作封裝在 Cache 接口中。它們之間的組織關(guān)系,大概如下圖:

2??一級緩存的生命周期

  1. MyBatis 在開啟一個數(shù)據(jù)庫會話時,會創(chuàng)建一個新的 SqlSession 對象,SqlSession 對象中會有一個新的 Executor 對象,Executor 對象中持有一個新的 PerpetualCache 對象(Cache 接口的實現(xiàn)類);當會話結(jié)束時,SqlSession 對象及其內(nèi)部的 Executor 對象還有 PerpetualCache 對象也一并釋放掉。
  2. 如果 SqlSession 調(diào)用了 close(),會釋放掉一級緩存 PerpetualCache 對象,一級緩存將不可用。
  3. 如果 SqlSession 調(diào)用了 clearCache(),會清空 PerpetualCache 對象中的數(shù)據(jù),但是 SqlSession 對象仍可使用。
  4. SqlSession 中執(zhí)行任何一個增/刪/改操作之后執(zhí)行事務提交 commit() ,都會清空PerpetualCache 對象的數(shù)據(jù),但是 SqlSession 對象可以繼續(xù)使用。

四、MyBatis 的二級緩存

1??二級緩存使用場景

類似于統(tǒng)計排行榜的查詢,可能會涉及到多張表很多字段的查詢統(tǒng)計排序,是非常費時費力的。如果每次都去數(shù)據(jù)庫查詢顯示一次排行榜數(shù)據(jù),那到排行榜這里,必定會卡頓很久,而且這種卡頓是用戶不能忍受的。做成一級緩存也是不可行的,每次 SqlSession 請求,每個客戶上來難道都要卡頓一次嗎?所以,這種查詢肯定要做成全局的緩存,當應用啟動的時候就緩存這種查詢數(shù)據(jù),然后每一周刷新一次這種數(shù)據(jù)就可以了。

由此,簡單總結(jié)二級緩存的特點和使用場景:二級緩存作用于全局,對于一些相當消耗性能的,并且對于時效性不敏感的查詢可以使用二級緩存。注意,如果開啟了二級緩存,查詢的順序是二級緩存 → 一級緩存 → 數(shù)據(jù)庫。

2??MyBatis 二級緩存的配置

在 MyBatis 中使用二級緩存就必須要進行配置了,必須要有下面的步驟才能正常使用二級緩存:

  1. 在全局設(shè)置中開啟二級緩存
<settings>...
  <!-- 開啟二級緩存 -->
  <setting name="cacheEnabled" value="true"/>
  ...
</settings>
  1. 在 xxxMapper.xml 中開啟 <cache> 標簽
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">
</cache>

可以簡寫為:

<cache/>

這樣就表示在 xxxMapper.xml 中開啟二級緩存了,因為 <cache/> 標簽的每個屬性都有默認值。cache 標簽屬性:

eviction:緩存回收策略,這個屬性又有下面幾個值
LRU – 最近最少使用的。移除最長時間不被使用的對象。
FIFO – 先進先出。按對象進入緩存的順序來移除它們。
SOFT – 軟引用。移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象。
WEAK – 弱引用。更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。
默認是LRU
flushInterval:刷新間隔,可以被設(shè)置為任意的正整數(shù),而且它們代表一個合理的毫秒 形式的時間段。默認情況是不設(shè)置,也就是沒有刷新間隔,緩存僅僅調(diào)用語句時刷新。
size:引用數(shù)目,可以被設(shè)置為任意正整數(shù),要記住你緩存的對象數(shù)目和你運行環(huán)境的 可用內(nèi)存資源數(shù)目。默認值是 1024。
readOnly:只讀屬性可以被設(shè)置為 true 或 false。只讀的緩存會給所有調(diào)用者返回緩 存對象的相同實例。因此這些對象不能被修改。這提供了很重要的性能優(yōu)勢??勺x寫的緩存會返回緩存對象的拷貝(通過序列化) 。這會慢一些,但是安全,因此默認是 false。

  1. 相關(guān)實體類需要序列化

放入二級緩存中保存的 JavaBean 需要實現(xiàn) Serializable 接口。序列化的意思就是從內(nèi)存中的數(shù)據(jù)傳到硬盤中。反序列化意思相反。MyBatis 的二級緩存,實際上就是將數(shù)據(jù)放進了硬盤文件中去了。

如果要使用 MyBatis 的二級緩存,除了要在需要緩存的 mapper.xml 中開啟以外,還需要目標實體類實現(xiàn)序列化的接口。當實體類有父類或級聯(lián)屬性,也必須實現(xiàn)序列化。

  1. useCache 和 flushCache

這一步不是必須的。這兩個都是屬于查詢標簽 <select> 的屬性

userCache 是用來設(shè)置是否禁用二級緩存的,在 statement 中設(shè)置 useCache=false 可以禁用當前 select 語句的二級緩存,即每次查詢都會發(fā)出 sql 去查詢,默認情況是 true,即該 sql 使用二級緩存。

flushCache 屬性,默認情況下為 true,即刷新緩存,如果改成 false 則不會刷新。使用緩存時如果手動修改數(shù)據(jù)庫表中的查詢數(shù)據(jù)會出現(xiàn)臟讀。

五、Mybatis 涉及的設(shè)計模式

1??Builder模式,例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
2??工廠模式,例如 SqlSessionFactory、ObjectFactory、MapperProxyFactory
3??單例模式,例如 ErrorContext 和 LogFactory
4??代理模式,Mybatis 實現(xiàn)的核心,比如 MapperProxy、ConnectionLogger,用的 jdk 的動態(tài)代理;還有 executor.loader 包使用了 cglib 或者 javassist 達到延遲加載的效果
5??組合模式,例如 SqlNode 和各個子類 ChooseSqlNode 等
6??模板方法模式,例如 BaseExecutor 和 SimpleExecutor,還有 BaseTypeHandler 和所有的子類例如 IntegerTypeHandler
7??適配器模式,例如 Log 的 Mybatis 接口和它對 jdbc、log4j 等各種日志框架的適配實現(xiàn)
8??裝飾者模式,例如 Cache 包中的 cache.decorators 子包中等各個裝飾者的實現(xiàn)
9??迭代器模式,例如迭代器模式 PropertyTokenizer

六、總結(jié)

  1. 進行 select 后,調(diào)用SqlSession.close(),會將其一級緩存的數(shù)據(jù)放進二級緩存中,此時一級緩存隨著 SqlSession 的關(guān)閉也就不存在了。
  2. 進行 select 后,調(diào)用SqlSession.commit(),會將其一級緩存的數(shù)據(jù)放進二級緩存中,并清空一級緩存。
  3. 對 SqlSession 執(zhí)行更新(insert、delete、update)后,同時不調(diào)用SqlSession.commit/SqlSession.close(),這時只會清空其自身的一級緩存,對二級緩存沒有影響。
  4. 對 SqlSession 執(zhí)行更新(insert、delete、update)后,執(zhí)行SqlSession.commit(),不僅清空其自身的一級緩存(執(zhí)行更新操作的結(jié)果),也清空二級緩存(執(zhí)行 commit() 的效果)。
  5. 對 SqlSession 執(zhí)行更新(insert、delete、update)后,執(zhí)行SqlSession.close()(沒有執(zhí)行 SqlSession.commit()),需分兩類情況。當 autoCommit 為 false 時,只會清空其自身的一級緩存(執(zhí)行更新操作的效果),對二級緩存沒有影響。當 autoCommit 為 true 時,會清空二級緩存。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容