基於我的的興趣,爲你們分享Mybatis的一級緩存以及二級緩存的特性。java
本次分析中涉及到的代碼和數據庫表均放在Github上,地址: mybatis-cache-demo。git
爲達到以上三個目的,本文按照如下順序展開。github
本章節會對Mybatis進行大致的介紹,分爲官方定義和核心組件介紹。
首先是Mybatis官方定義,以下所示。算法
MyBatis是支持定製化SQL、存儲過程以及高級映射的優秀的持久層框架。MyBatis避免了幾乎全部的JDBC代碼和手動設置參數以及獲取結果集。MyBatis能夠對配置和原生Map使用簡單的XML或註解,將接口和Java 的POJOs(Plain Old Java Objects,普通的 Java對象)映射成數據庫中的記錄。sql
其次是Mybatis的幾個核心概念。數據庫
下圖就是一個針對Student表操做的接口文件StudentMapper,在StudentMapper中,咱們能夠若干方法,這個方法背後就是表明着要執行的Sql的意義。
緩存
在Mybatis初始化的時候,每個語句都會使用對應的MappedStatement表明,使用namespace+語句自己的id來表明這個語句。以下代碼所示,使用mapper.StudentMapper.getStudentById表明其對應的Sql。安全
SELECT id,name,age FROM student WHERE id = #{id}複製代碼
在Mybatis執行時,會進入對應接口的方法,經過類名加上方法名的組合生成id,找到須要的MappedStatement,交給執行器使用。
至此,Mybatis的基礎概念介紹完畢。bash
在系統代碼的運行中,咱們可能會在一個數據庫會話中,執行屢次查詢條件徹底相同的Sql,鑑於平常應用的大部分場景都是讀多寫少,這重複的查詢會帶來必定的網絡開銷,同時select查詢的量比較大的話,對數據庫的性能是有比較大的影響的。網絡
若是是Mysql數據庫的話,在服務端和Jdbc端都開啓預編譯支持的話,能夠在本地JVM端緩存Statement,能夠在Mysql服務端直接執行Sql,省去編譯Sql的步驟,但也沒法避免和數據庫之間的重複交互。關於Jdbc和Mysql預編譯緩存的事情,能夠看個人這篇博客JDBC和Mysql那些事。
Mybatis提供了一級緩存的方案來優化在數據庫會話間重複查詢的問題。實現的方式是每個SqlSession中都持有了本身的緩存,一種是SESSION級別,即在一個Mybatis會話中執行的全部語句,都會共享這一個緩存。一種是STATEMENT級別,能夠理解爲緩存只對當前執行的這一個statement有效。若是用一張圖來表明一級查詢的查詢過程的話,能夠用下圖表示。
上文介紹了一級緩存的實現方式,解決了什麼問題。在這個章節,咱們學習如何使用Mybatis的一級緩存。只須要在Mybatis的配置文件中,添加以下語句,就可使用一級緩存。共有兩個選項,SESSION或者STATEMENT,默認是SESSION級別。
<setting name="localCacheScope" value="SESSION"/>複製代碼
配置完畢後,經過實驗的方式瞭解Mybatis一級緩存的效果。每個單元測試後都請恢復被修改的數據。
首先是建立了一個示例表student,爲其建立了對應的POJO類和增改的方法,具體能夠在entity包和Mapper包中查看。
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
`age` tinyint(3) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;複製代碼
在如下實驗中,id爲1的學生名稱是凱倫。
開啓一級緩存,範圍爲會話級別,調用三次getStudentById,代碼以下所示:
public void getStudentById() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自動提交事務
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
}複製代碼
執行結果:
在此次的試驗中,咱們增長了對數據庫的修改操做,驗證在一次數據庫會話中,對數據庫發生了修改操做,一級緩存是否會失效。
@Test
public void addStudent() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自動提交事務
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println("增長了" + studentMapper.addStudent(buildStudent()) + "個學生");
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}複製代碼
執行結果:
開啓兩個SqlSession,在sqlSession1中查詢數據,使一級緩存生效,在sqlSession2中更新數據庫,驗證一級緩存只在數據庫會話內部共享。
@Test
public void testLocalCacheScope() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "個學生的數據");
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}複製代碼
這一章節主要從一級緩存的工做流程和源碼層面對一級緩存進行學習。
根據一級緩存的工做流程,咱們繪製出一級緩存執行的時序圖,以下圖所示。
4.1 去數據庫中查詢數據,獲得查詢結果;
4.2 將key和查詢到的結果做爲key和value,放入Local Cache中。
4.3. 將查詢結果返回;複製代碼
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;複製代碼
在一級緩存的介紹中,咱們提到對Local Cache的查詢和寫入是在Executor內部完成的。在閱讀BaseExecutor的代碼後,咱們也發現Local Cache就是它內部的一個成員變量,以下代碼所示。public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;複製代碼
Cache: Mybatis中的Cache接口,提供了和緩存相關的最基本的操做,有若干個實現類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力。public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();複製代碼
在閱讀相關核心類代碼後,從源代碼層面對一級緩存工做中涉及到的相關代碼,出於篇幅的考慮,對源碼作適當刪減,讀者朋友能夠結合本文,後續進行更詳細的學習。
爲了執行和數據庫的交互,首先會經過DefaultSqlSessionFactory開啓一個SqlSession,在建立SqlSession的過程當中,會經過Configuration類建立一個全新的Executor,做爲DefaultSqlSession構造函數的參數,代碼以下所示。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
............
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}複製代碼
若是用戶不進行制定的話,Configuration在建立Executor時,默認建立的類型就是SimpleExecutor,它是一個簡單的執行類,只是單純執行Sql。如下是具體用來建立的代碼。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor 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);
}
// 尤爲能夠注意這裏,若是二級緩存開關開啓的話,是使用CahingExecutor裝飾BaseExecutor的子類
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}複製代碼
在SqlSession建立完畢後,根據Statment的不一樣類型,會進入SqlSession的不一樣方法中,若是是Select語句的話,最後會執行到SqlSession的selectList,代碼以下所示。
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}複製代碼
在上文的代碼中,SqlSession把具體的查詢職責委託給了Executor。若是隻開啓了一級緩存的話,首先會進入BaseExecutor的query方法。代碼以下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}複製代碼
在上述代碼中,會先根據傳入的參數生成CacheKey,進入該方法查看CacheKey是如何生成的,代碼以下所示。
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//後面是update了sql中帶的參數
cacheKey.update(value);複製代碼
在上述的代碼中,咱們能夠看到它將MappedStatement的Id、sql的offset、Sql的limit、Sql自己以及Sql中的參數傳入了CacheKey這個類,最終生成了CacheKey。咱們看一下這個類的結構。
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}複製代碼
首先是它的成員變量和構造函數,有一個初始的hachcode和乘數,同時維護了一個內部的updatelist。在CacheKey的update方法中,會進行一個hashcode和checksum的計算,同時把傳入的參數添加進updatelist中。以下代碼所示。
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}複製代碼
咱們是如何判斷CacheKey相等的呢,在CacheKey的equals方法中給了咱們答案,代碼以下所示。
@Override
public boolean equals(Object object) {
.............
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}複製代碼
除去hashcode,checksum和count的比較外,只要updatelist中的元素一一對應相等,那麼就能夠認爲是CacheKey相等。只要兩條Sql的下列五個值相同,便可以認爲是相同的Sql。
Statement Id + Offset + Limmit + Sql + Params
BaseExecutor的query方法繼續往下走,代碼以下所示。
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 這個主要是處理存儲過程用的。
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}複製代碼
若是查不到的話,就從數據庫查,在queryFromDatabase中,會對localcache進行寫入。
在query方法執行的最後,會判斷一級緩存級別是不是STATEMENT級別,若是是的話,就清空緩存,這也就是STATEMENT級別的一級緩存沒法共享localCache的緣由。代碼以下所示。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}複製代碼
在源碼分析的最後,咱們確認一下,若是是insert/delete/update方法,緩存就會刷新的緣由。
SqlSession的insert方法和delete方法,都會統一走update的流程,代碼以下所示。
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}複製代碼
update方法也是委託給了Executor執行。BaseExecutor的執行方法以下所示。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}複製代碼
每次執行update前都會清空localCache。
至此,一級緩存的工做流程講解以及源碼分析完畢。
在上文中提到的一級緩存中,其最大的共享範圍就是一個SqlSession內部,那麼如何讓多個SqlSession之間也能夠共享緩存呢,答案是二級緩存。
當開啓二級緩存後,會使用CachingExecutor裝飾Executor,在進入後續執行前,先在CachingExecutor進行二級緩存的查詢,具體的工做流程以下所示。
要正確的使用二級緩存,需完成以下配置的。
1 在Mybatis的配置文件中開啓二級緩存。
<setting name="cacheEnabled" value="true"/>複製代碼
2 在Mybatis的映射XML中配置cache或者 cache-ref 。
<cache/>複製代碼
cache標籤用於聲明這個namespace使用二級緩存,而且能夠自定義配置。
<cache-ref namespace="mapper.StudentMapper"/>複製代碼
cache-ref表明引用別的命名空間的Cache配置,兩個命名空間的操做使用的是同一個Cache。
在本章節,經過實驗,瞭解Mybatis二級緩存在使用上的一些特色。
在本實驗中,id爲1的學生名稱初始化爲點點。
測試二級緩存效果,不提交事務,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。
@Test
public void testCacheWithoutCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}複製代碼
執行結果:
測試二級緩存效果,當提交事務時,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。
@Test
public void testCacheWithCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}複製代碼
測試update操做是否會刷新該namespace下的二級緩存。
@Test
public void testCacheWithUpdate() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
studentMapper3.updateStudentName("方方",1);
sqlSession3.commit();
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}複製代碼
驗證Mybatis的二級緩存不適應用於映射文件中存在多表查詢的狀況。通常來講,咱們會爲每個單表建立一個單獨的映射文件,若是存在涉及多個表的查詢的話,因爲Mybatis的二級緩存是基於namespace的,多表查詢語句所在的namspace沒法感應到其餘namespace中的語句對多表查詢中涉及的表進行了修改,引起髒數據問題。
@Test
public void testCacheWithDiffererntNamespace() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);
System.out.println("studentMapper讀取數據: " + studentMapper.getStudentByIdWithClassInfo(1));
sqlSession1.close();
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));
classMapper.updateClassName("特點一班",1);
sqlSession3.commit();
System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));
}複製代碼
執行結果:
爲了解決實驗4的問題呢,可使用Cache ref,讓ClassMapper引用StudenMapper命名空間,這樣兩個映射文件對應的Sql操做都使用的是同一塊緩存了。
執行結果:
Mybatis二級緩存的工做流程和前文提到的一級緩存相似,只是在一級緩存處理前,用CachingExecutor裝飾了BaseExecutor的子類,實現了緩存的查詢和寫入功能,因此二級緩存直接從源碼開始分析。
源碼分析從CachingExecutor的query方法展開,源代碼走讀過程當中涉及到的知識點較多,不能一一詳細講解,能夠在文後留言,我會在交流環節更詳細的表示出來。
CachingExecutor的query方法,首先會從MappedStatement中得到在配置初始化時賦予的cache。
Cache cache = ms.getCache();複製代碼
本質上是裝飾器模式的使用,具體的執行鏈是
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
而後是判斷是否須要刷新緩存,代碼以下所示。
flushCacheIfRequired(ms);複製代碼
在默認的設置中SELECT語句不會刷新緩存,insert/update/delte會刷新緩存。進入該方法。代碼以下所示。
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}複製代碼
Mybatis的CachingExecutor持有了TransactionalCacheManager,即上述代碼中的tcm。
TransactionalCacheManager中持有了一個Map,代碼以下所示。
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();複製代碼
這個Map保存了Cache和用TransactionalCache包裝後的Cache的映射關係。
TransactionalCache實現了Cache接口,CachingExecutor會默認使用他包裝初始生成的Cache,做用是若是事務提交,對緩存的操做纔會生效,若是事務回滾或者不提交事務,則不對緩存產生影響。
在TransactionalCache的clear,有如下兩句。清空了須要在提交時加入緩存的列表,同時設定提交時清空緩存,代碼以下所示。
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}複製代碼
CachingExecutor繼續往下走,ensureNoOutParams主要是用來處理存儲過程的,暫時不用考慮。
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);複製代碼
以後會嘗試從tcm中獲取緩存的列表。
List<E> list = (List<E>) tcm.getObject(cache, key);複製代碼
在getObject方法中,會把獲取值的職責一路向後傳,最終到PerpetualCache。若是沒有查到,會把key加入Miss集合,這個主要是爲了統計命中率。
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}複製代碼
CachingExecutor繼續往下走,若是查詢到數據,則調用tcm.putObject方法,往緩存中放入值。
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}複製代碼
tcm的put方法也不是直接操做緩存,只是在把此次的數據和key放入待提交的Map中。
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}複製代碼
從以上的代碼分析中,咱們能夠明白,若是不調用commit方法的話,因爲TranscationalCache的做用,並不會對二級緩存形成直接的影響。所以咱們看看Sqlsession的commit方法中作了什麼。代碼以下所示。
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));複製代碼
由於咱們使用了CachingExecutor,首先會進入CachingExecutor實現的commit方法。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}複製代碼
會把具體commit的職責委託給包裝的Executor。主要是看下tcm.commit(),tcm最終又會調用到TrancationalCache。
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}複製代碼
看到這裏的clearOnCommit就想起剛纔TrancationalCache的clear方法設置的標誌位,真正的清理Cache是放到這裏來進行的。具體清理的職責委託給了包裝的Cache類。以後進入flushPendingEntries方法。代碼以下所示。
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
................
}複製代碼
在flushPendingEntries中,就把待提交的Map循環後,委託給包裝的Cache類,進行putObject的操做。
後續的查詢操做會重複執行這套流程。若是是insert|update|delete的話,會統一進入CachingExecutor的update方法,其中調用了這個函數,代碼以下所示,所以再也不贅述。
private void flushCacheIfRequired(MappedStatement ms)複製代碼