一、簡述
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 對象之中。


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??一級緩存的生命周期

- MyBatis 在開啟一個數(shù)據(jù)庫會話時,會創(chuàng)建一個新的 SqlSession 對象,SqlSession 對象中會有一個新的 Executor 對象,Executor 對象中持有一個新的 PerpetualCache 對象(Cache 接口的實現(xiàn)類);當會話結(jié)束時,SqlSession 對象及其內(nèi)部的 Executor 對象還有 PerpetualCache 對象也一并釋放掉。
- 如果 SqlSession 調(diào)用了 close(),會釋放掉一級緩存 PerpetualCache 對象,一級緩存將不可用。
- 如果 SqlSession 調(diào)用了 clearCache(),會清空 PerpetualCache 對象中的數(shù)據(jù),但是 SqlSession 對象仍可使用。
- 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 中使用二級緩存就必須要進行配置了,必須要有下面的步驟才能正常使用二級緩存:
- 在全局設(shè)置中開啟二級緩存
<settings>...
<!-- 開啟二級緩存 -->
<setting name="cacheEnabled" value="true"/>
...
</settings>
- 在 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。
- 相關(guān)實體類需要序列化
放入二級緩存中保存的 JavaBean 需要實現(xiàn) Serializable 接口。序列化的意思就是從內(nèi)存中的數(shù)據(jù)傳到硬盤中。反序列化意思相反。MyBatis 的二級緩存,實際上就是將數(shù)據(jù)放進了硬盤文件中去了。
如果要使用 MyBatis 的二級緩存,除了要在需要緩存的 mapper.xml 中開啟以外,還需要目標實體類實現(xiàn)序列化的接口。當實體類有父類或級聯(lián)屬性,也必須實現(xiàn)序列化。
- 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é)
- 進行 select 后,調(diào)用
SqlSession.close(),會將其一級緩存的數(shù)據(jù)放進二級緩存中,此時一級緩存隨著 SqlSession 的關(guān)閉也就不存在了。 - 進行 select 后,調(diào)用
SqlSession.commit(),會將其一級緩存的數(shù)據(jù)放進二級緩存中,并清空一級緩存。 - 對 SqlSession 執(zhí)行更新(insert、delete、update)后,同時不調(diào)用
SqlSession.commit/SqlSession.close(),這時只會清空其自身的一級緩存,對二級緩存沒有影響。 - 對 SqlSession 執(zhí)行更新(insert、delete、update)后,執(zhí)行
SqlSession.commit(),不僅清空其自身的一級緩存(執(zhí)行更新操作的結(jié)果),也清空二級緩存(執(zhí)行 commit() 的效果)。 - 對 SqlSession 執(zhí)行更新(insert、delete、update)后,執(zhí)行
SqlSession.close()(沒有執(zhí)行 SqlSession.commit()),需分兩類情況。當 autoCommit 為 false 時,只會清空其自身的一級緩存(執(zhí)行更新操作的效果),對二級緩存沒有影響。當 autoCommit 為 true 時,會清空二級緩存。