前言
MyBatis是常見的Java數據庫訪問層框架。在平常工做中,開發人員多數狀況下是使用MyBatis的默認緩存配置,可是MyBatis緩存機制有一些不足之處,在使用中容易引發髒數據,造成一些潛在的隱患。我的在業務開發中也處理過一些因爲MyBatis緩存引起的開發問題,帶着我的的興趣,但願從應用及源碼的角度爲讀者梳理MyBatis緩存機制。html
本次分析中涉及到的代碼和數據庫表均放在GitHub上,地址: mybatis-cache-demo 。前端
目錄
本文按照如下順序展開。java
- 一級緩存介紹及相關配置。
- 一級緩存工做流程及源碼分析。
- 一級緩存總結。
- 二級緩存介紹及相關配置。
- 二級緩存源碼分析。
- 二級緩存總結。
- 全文總結。
一級緩存
一級緩存介紹
在應用運行過程當中,咱們有可能在一次數據庫會話中,執行屢次查詢條件徹底相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,若是是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提升性能。具體執行過程以下圖所示。git
每一個SqlSession中持有了Executor,每一個Executor中有一個LocalCache。當用戶發起查詢時,MyBatis根據當前執行的語句生成MappedStatement
,在Local Cache進行查詢,若是緩存命中的話,直接返回結果給用戶,若是緩存沒有命中的話,查詢數據庫,結果寫入Local Cache
,最後返回結果給用戶。具體實現類的類關係圖以下圖所示。github
一級緩存配置
咱們來看看如何使用MyBatis一級緩存。開發者只需在MyBatis的配置文件中,添加以下語句,就可使用一級緩存。共有兩個選項,SESSION
或者STATEMENT
,默認是SESSION
級別,即在一個MyBatis會話中執行的全部語句,都會共享這一個緩存。一種是STATEMENT
級別,能夠理解爲緩存只對當前執行的這一個Statement
有效。算法
<setting name="localCacheScope" value="SESSION"/>
一級緩存實驗
接下來經過實驗,瞭解MyBatis一級緩存的效果,每一個單元測試後都請恢復被修改的數據。sql
首先是建立示例表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的學生名稱是凱倫。後端
實驗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)); }
執行結果:
咱們能夠看到,只有第一次真正查詢了數據庫,後續的查詢使用了一級緩存。
實驗2
增長了對數據庫的修改操做,驗證在一次數據庫會話中,若是對數據庫發生了修改操做,一級緩存是否會失效。
@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(); }
執行結果:
咱們能夠看到,在修改操做後執行的相同查詢,查詢了數據庫,一級緩存失效。
實驗3
開啓兩個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)); }
sqlSession2
更新了id爲1的學生的姓名,從凱倫改成了小岑,但session1以後的查詢中,id爲1的學生的名字仍是凱倫,出現了髒數據,也證實了以前的設想,一級緩存只在數據庫會話內部共享。
一級緩存工做流程&源碼分析
那麼,一級緩存的工做流程是怎樣的呢?咱們從源碼層面來學習一下。
工做流程
一級緩存執行的時序圖,以下圖所示。
源碼分析
接下來將對MyBatis查詢相關的核心類和一級緩存的源碼進行走讀。這對後面學習二級緩存也有幫助。
SqlSession: 對外提供了用戶和數據庫之間交互須要的全部方法,隱藏了底層的細節。默認實現類是DefaultSqlSession
。
Executor: SqlSession
向用戶提供操做數據庫的方法,但和數據庫操做有關的職責都會委託給Executor。
以下圖所示,Executor有若干個實現類,爲Executor賦予了不一樣的能力,你們能夠根據類名,自行學習每一個類的基本做用。
在一級緩存的源碼分析中,主要學習BaseExecutor
的內部實現。
BaseExecutor: BaseExecutor
是一個實現了Executor接口的抽象類,定義若干抽象方法,在執行的時候,把具體的操做委託給子類進行執行。
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
是BaseExecutor
內部的一個成員變量,以下代碼所示。
public abstract class BaseExecutor implements Executor { protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; protected PerpetualCache localCache;
Cache: MyBatis中的Cache接口,提供了和緩存相關的最基本的操做,以下圖所示:
有若干個實現類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力,部分實現類以下圖所示:
BaseExecutor
成員變量之一的PerpetualCache
,是對Cache接口最基本的實現,其實現很是簡單,內部持有HashMap,對一級緩存的操做實則是對HashMap的操做。以下代碼所示:
public class PerpetualCache implements Cache { private String id; private Map<Object, Object> cache = new HashMap<Object, Object>();
在閱讀相關核心類代碼後,從源代碼層面對一級緩存工做中涉及到的相關代碼,出於篇幅的考慮,對源碼作適當刪減,讀者朋友能夠結合本文,後續進行更詳細的學習。
爲執行和數據庫的交互,首先須要初始化SqlSession
,經過DefaultSqlSessionFactory
開啓SqlSession
:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { ............ final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); }
在初始化SqlSesion
時,會使用Configuration
類建立一個全新的Executor
,做爲DefaultSqlSession
構造函數的參數,建立Executor代碼以下所示:
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
的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
。
至此,一級緩存的工做流程講解以及源碼分析完畢。
總結
- MyBatis一級緩存的生命週期和SqlSession一致。
- MyBatis一級緩存內部設計簡單,只是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
- MyBatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,數據庫寫操做會引發髒數據,建議設定緩存級別爲Statement。
二級緩存
二級緩存介紹
在上文中提到的一級緩存中,其最大的共享範圍就是一個SqlSession內部,若是多個SqlSession之間須要共享緩存,則須要使用到二級緩存。開啓二級緩存後,會使用CachingExecutor裝飾Executor,進入一級緩存的查詢流程前,先在CachingExecutor進行二級緩存的查詢,具體的工做流程以下所示。
二級緩存開啓後,同一個namespace下的全部操做語句,都影響着同一個Cache,即二級緩存被多個SqlSession共享,是一個全局的變量。
當開啓緩存後,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。
二級緩存配置
要正確的使用二級緩存,需完成以下配置的。
- 在MyBatis的配置文件中開啓二級緩存。
<setting name="cacheEnabled" value="true"/>
- 在MyBatis的映射XML中配置cache或者 cache-ref 。
cache標籤用於聲明這個namespace使用二級緩存,而且能夠自定義配置。
<cache/>
type
:cache使用的類型,默認是PerpetualCache
,這在一級緩存中提到過。eviction
: 定義回收的策略,常見的有FIFO,LRU。flushInterval
: 配置必定時間自動刷新緩存,單位是毫秒。size
: 最多緩存對象的個數。readOnly
: 是否只讀,若配置可讀寫,則須要對應的實體類可以序列化。blocking
: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。
cache-ref
表明引用別的命名空間的Cache配置,兩個命名空間的操做使用的是同一個Cache。
<cache-ref namespace="mapper.StudentMapper"/>
二級緩存實驗
接下來咱們經過實驗,瞭解MyBatis二級緩存在使用上的一些特色。
在本實驗中,id爲1的學生名稱初始化爲點點。
實驗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)); }
執行結果:
咱們能夠看到,當sqlsession
沒有調用commit()
方法時,二級緩存並無起到做用。
實驗2
測試二級緩存效果,當提交事務時,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)); }
從圖上可知,sqlsession2
的查詢,使用了緩存,緩存的命中率是0.5。
實驗3
測試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)); }
咱們能夠看到,在sqlSession3
更新數據庫,並提交事務後,sqlsession2
的StudentMapper namespace
下的查詢走了數據庫,沒有走Cache。
實驗4
驗證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)); }
執行結果:
在這個實驗中,咱們引入了兩張新的表,一張class,一張classroom。class中保存了班級的id和班級名,classroom中保存了班級id和學生id。咱們在StudentMapper
中增長了一個查詢方法getStudentByIdWithClassInfo
,用於查詢學生所在的班級,涉及到多表查詢。在ClassMapper
中添加了updateClassName
,根據班級id更新班級名的操做。
當sqlsession1
的studentmapper
查詢數據後,二級緩存生效。保存在StudentMapper的namespace下的cache中。當sqlSession3
的classMapper
的updateClassName
方法對class表進行更新時,updateClassName
不屬於StudentMapper
的namespace
,因此StudentMapper
下的cache沒有感應到變化,沒有刷新緩存。當StudentMapper
中一樣的查詢再次發起時,從緩存中讀取了髒數據。
實驗5
爲了解決實驗4的問題呢,可使用Cache ref,讓ClassMapper
引用StudenMapper
命名空間,這樣兩個映射文件對應的SQL操做都使用的是同一塊緩存了。
執行結果:
不過這樣作的後果是,緩存的粒度變粗了,多個Mapper namespace
下的全部操做都會對緩存使用形成影響。
二級緩存源碼分析
MyBatis二級緩存的工做流程和前文提到的一級緩存相似,只是在一級緩存處理前,用CachingExecutor
裝飾了BaseExecutor
的子類,在委託具體職責給delegate
以前,實現了二級緩存的查詢和寫入功能,具體類關係圖以下圖所示。
源碼分析
源碼分析從CachingExecutor
的query
方法展開,源代碼走讀過程當中涉及到的知識點較多,不能一一詳細講解,讀者朋友能夠自行查詢相關資料來學習。
CachingExecutor
的query
方法,首先會從MappedStatement
中得到在配置初始化時賦予的Cache。
Cache cache = ms.getCache();
本質上是裝飾器模式的使用,具體的裝飾鏈是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
如下是具體這些Cache實現類的介紹,他們的組合爲Cache賦予了不一樣的能力。
SynchronizedCache
:同步Cache,實現比較簡單,直接使用synchronized修飾方法。LoggingCache
:日誌功能,裝飾類,用於記錄緩存的命中率,若是開啓了DEBUG模式,則會輸出命中率日誌。SerializedCache
:序列化功能,將值序列化後存到緩存中。該功能用於緩存返回一份實例的Copy,用於保存線程安全。LruCache
:採用了Lru算法的Cache實現,移除最近最少使用的Key/Value。PerpetualCache
: 做爲爲最基礎的緩存類,底層實現比較簡單,直接使用了HashMap。
而後是判斷是否須要刷新緩存,代碼以下所示:
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()); } ................ }
在flushPending
Entries中,將待提交的Map進行循環處理,委託給包裝的Cache類,進行putObject
的操做。
後續的查詢操做會重複執行這套流程。若是是insert|update|delete
的話,會統一進入CachingExecutor
的update
方法,其中調用了這個函數,代碼以下所示:
private void flushCacheIfRequired(MappedStatement ms)
在二級緩存執行流程後就會進入一級緩存的執行流程,所以再也不贅述。
總結
- MyBatis的二級緩存相對於一級緩存來講,實現了
SqlSession
之間緩存數據的共享,同時粒度更加的細,可以到namespace
級別,經過Cache接口實現類不一樣的組合,對Cache的可控性也更強。 - MyBatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
- 在分佈式環境下,因爲默認的MyBatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,須要使用集中式緩存將MyBatis的Cache接口實現,有必定的開發成本,直接使用Redis、Memcached等分佈式緩存可能成本更低,安全性也更高。
全文總結
本文對介紹了MyBatis一二級緩存的基本概念,並從應用及源碼的角度對MyBatis的緩存機制進行了分析。最後對MyBatis緩存機制作了必定的總結,我的建議MyBatis緩存特性在生產環境中進行關閉,單純做爲一個ORM框架使用可能更爲合適。
做者簡介
- 凱倫,美團點評後端研發工程師,2016年畢業於上海海事大學,現從事美團點評餐飲平臺相關的開發工做。
招聘信息
美團點評點餐事業部期待你的加入,上海在招崗位:Java後臺,數據開發,前端,QA,產品,產品運營,商業分析等。內推簡歷郵箱:weiyanping#meituan.com
如發現文章有錯誤、對內容有疑問,均可以關注美團技術團隊微信公衆號(meituantech),在後臺給咱們留言。
咱們每週會挑選出一位熱心小夥伴,送上一份精美的小禮品。快來掃碼關注咱們吧!