關於 Mybatis 快取機制,面試官都未必知道的這麼詳細
歡迎關注個人微信公眾號: 小哈學Java, 文末分享阿里 P8 高階架構師吐血總結的 《Java 核心知識整理&面試.pdf》資源連結!!
個人網站: https://www.exception.site
本文轉載自: https://tech.meituan.com/2018/01/19/mybatis-cache.html
一、前言
MyBatis是常見的Java資料庫訪問層框架。在日常工作中,開發人員多數情況下是使用MyBatis的預設快取配置,但是MyBatis快取機制有一些不足之處,在使用中容易引起髒資料,形成一些潛在的隱患。個人在業務開發中也處理過一些由於MyBatis快取引發的開發問題,帶著個人的興趣,希望從應用及原始碼的角度為讀者梳理MyBatis快取機制。
本次分析中涉及到的程式碼和資料庫表均放在GitHub上,地址: mybatis-cache-demo 。
二、目錄
本文按照以下順序展開。
- 一級快取介紹及相關配置。
- 一級快取工作流程及原始碼分析。
- 一級快取總結。
- 二級快取介紹及相關配置。
- 二級快取原始碼分析。
- 二級快取總結。
- 全文總結。
三、一級快取
3.1 一級快取介紹
在應用執行過程中,我們有可能在一次資料庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級快取的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級快取,避免直接對資料庫進行查詢,提高效能。具體執行過程如下圖所示。
每個SqlSession中持有了Executor,每個Executor中有一個LocalCache。當用戶發起查詢時,MyBatis根據當前執行的語句生成MappedStatement
Local Cache
,最後返回結果給使用者。具體實現類的類關係圖如下圖所示。
3.2 一級快取配置
我們來看看如何使用MyBatis一級快取。開發者只需在MyBatis的配置檔案中,新增如下語句,就可以使用一級快取。共有兩個選項,SESSION
或者STATEMENT
,預設是SESSION
級別,即在一個MyBatis會話中執行的所有語句,都會共享這一個快取。一種是STATEMENT
級別,可以理解為快取只對當前執行的這一個Statement
有效。
<setting name="localCacheScope" value="SESSION"/>
3.3 一級快取實驗
接下來通過實驗,瞭解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的學生名稱是凱倫。
3.3.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));
}
執行結果:
我們可以看到,只有第一次真正查詢了資料庫,後續的查詢使用了一級快取。
3.3.2 實驗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.3.3 實驗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的學生的名字還是凱倫,出現了髒資料,也證明了之前的設想,一級快取只在資料庫會話內部共享。
3.4 一級快取工作流程&原始碼分析
那麼,一級快取的工作流程是怎樣的呢?我們從原始碼層面來學習一下。
3.4.1 工作流程
一級快取執行的時序圖,如下圖所示。
3.4.2 原始碼分析
接下來將對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
。
至此,一級快取的工作流程講解以及原始碼分析完畢。
3.5 總結
- MyBatis一級快取的生命週期和SqlSession一致。
- MyBatis一級快取內部設計簡單,只是一個沒有容量限定的HashMap,在快取的功能性上有所欠缺。
- MyBatis的一級快取最大範圍是SqlSession內部,有多個SqlSession或者分散式的環境下,資料庫寫操作會引起髒資料,建議設定快取級別為Statement。
四、二級快取
4.1 二級快取介紹
在上文中提到的一級快取中,其最大的共享範圍就是一個SqlSession內部,如果多個SqlSession之間需要共享快取,則需要使用到二級快取。開啟二級快取後,會使用CachingExecutor裝飾Executor,進入一級快取的查詢流程前,先在CachingExecutor進行二級快取的查詢,具體的工作流程如下所示。
二級快取開啟後,同一個namespace下的所有操作語句,都影響著同一個Cache,即二級快取被多個SqlSession共享,是一個全域性的變數。
當開啟快取後,資料的查詢執行的流程就是 二級快取 -> 一級快取 -> 資料庫。
4.2 二級快取配置
要正確的使用二級快取,需完成如下配置的。
- 在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"/>
4.3 二級快取實驗
接下來我們通過實驗,瞭解MyBatis二級快取在使用上的一些特點。
在本實驗中,id為1的學生名稱初始化為點點。
4.3.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()
方法時,二級快取並沒有起到作用。
4.3.2 實驗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。
4.3.3 實驗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.3.4 實驗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
中同樣的查詢再次發起時,從快取中讀取了髒資料。
4.3.5 實驗5
為了解決實驗4的問題呢,可以使用Cache ref,讓ClassMapper
引用StudenMapper
名稱空間,這樣兩個對映檔案對應的SQL操作都使用的是同一塊快取了。
執行結果:
不過這樣做的後果是,快取的粒度變粗了,多個Mapper namespace
下的所有操作都會對快取使用造成影響。
4.4 二級快取原始碼分析
MyBatis二級快取的工作流程和前文提到的一級快取類似,只是在一級快取處理前,用CachingExecutor
裝飾了BaseExecutor
的子類,在委託具體職責給delegate
之前,實現了二級快取的查詢和寫入功能,具體類關係圖如下圖所示。
4.4.1 原始碼分析
原始碼分析從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)
在二級快取執行流程後就會進入一級快取的執行流程,因此不再贅述。
4.5 總結
- MyBatis的二級快取相對於一級快取來說,實現了
SqlSession
之間快取資料的共享,同時粒度更加的細,能夠到namespace
級別,通過Cache介面實現類不同的組合,對Cache的可控性也更強。 - MyBatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用二級快取的條件比較苛刻。
- 在分散式環境下,由於預設的MyBatis Cache實現都是基於本地的,分散式環境下必然會出現讀取到髒資料,需要使用集中式快取將MyBatis的Cache介面實現,有一定的開發成本,直接使用Redis、Memcached等分散式快取可能成本更低,安全性也更高。
五、全文總結
本文對介紹了MyBatis一二級快取的基本概念,並從應用及原始碼的角度對MyBatis的快取機制進行了分析。最後對MyBatis快取機制做了一定的總結,個人建議MyBatis快取特性在生產環境中進行關閉,單純作為一個ORM框架使用可能更為合適。
免費分享 | 面試&學習福利資源
最近在網上發現一個不錯的 PDF 資源《Java 核心知識&面試.pdf》分享給大家,不光是面試,學習,你都值得擁有!!!
獲取方式: 關注公眾號: 小哈學Java, 後臺回覆資源,既可免費無套路獲取資源連結,下面是目錄以及部分截圖:
重要的事情說兩遍,關注公眾號: 小哈學Java, 後臺回覆資源,既可免費無套路獲取資源連結 !!!
歡迎關注微信公眾號: 小哈學Java
相關推薦
關於 Mybatis 快取機制,面試官都未必知道的這麼詳細
歡迎關注個人微信公眾號: 小哈學Java, 文末分享阿里 P8 高階架構師吐血總結的 《Java 核心知識整理&面試.pdf》資源連結!! 個人網站: https://www.exception.site 本文轉載自: https://tech.meituan.com/2018/01/19/myba
【面試複習系列】常用機器學習演算法知識點及其解析,面試官會考的幾乎都有,歡迎補充
圖片慢慢上傳,看不到圖片的請點這裡: LR:logistic regression 對數機率迴歸/邏輯迴歸 sigmoid函式的作用就是用於把輸出歸一到1和0,也就
面試官:都說阻塞 I/O 模型將會使執行緒休眠,為什麼 Java 執行緒狀態卻是 RUNNABLE?
摘要: 原創出處 https://studyidea.cn 「公眾號:程式通事 」歡迎關注和轉載,保留摘要,謝謝! 使用 Java 阻塞 I/O 模型讀取資料,將會導致執行緒阻塞,執行緒將會進入休眠,從而讓出 CPU 的執行權,直到資料讀取完成。這個期間如果使用 jstack 檢視執行緒狀態,卻可以發
你都這麼拼了,面試官TM怎麼還是無動於衷
面試,對於每個人而然並不陌生,可以說是必須經歷的一個過程了,小到一場考試,大到企業面試,甚至大型選秀。。。。。。 有時自己明明很努力了,但偏偏會在面試環節出了插曲,比如,緊張就是最容易出現的了。 我相信大部分同學,對自己的專業能力和技術水平,都是有絕對的自信的,可是為什麼到了面試官口試的時候掉鏈子了呢?
面試 HTTP ,99% 的面試官都愛問這些問題
HTTP 和 HTTPS 的區別 HTTP 是一種 超文字傳輸協議(Hypertext Transfer Protocol),HTTP 是一個在計算機世界裡專門在兩點之間傳輸文字、圖片、音訊、視訊等超文字資料的約定和規範 HTTP 主要內容分為三部分,超文字(Hypertext)、傳輸(Transfer)
MyBatis快取機制(一級快取,二級快取)
一,MyBatis一級快取(本地快取) My Batis 一級快取存在於 SqlSession 的生命週期中,是SqlSession級別的快取。在操作資料庫時需要構造SqlSession物件,在物件中有一個數據結構用來儲存快取資料。不同的SqlSession之間的資料快取是不能共享的。 在同一個Sql
【遞迴題】正確的開啟方式,面試官聽了都說精闢
## 前言 遞迴,是一個非常重要的概念,也是面試中非常喜歡考的。因為它不但能考察一個程式設計師的演算法功底,還能很好的考察對**時間空間複雜度**的理解和分析。 本文只講一題,也是幾乎所有演算法書講遞迴的第一題,但力爭講出花來,在這裡分享四點不一樣的角度,讓你有不同的收穫。 - **時空複雜度**的詳
騰訊面試,面試官第一個問題是Int佔多少位元組,程式設計師一臉懵逼
程式設計師面試什麼最重要? 程式設計師面試一直是社群樂於討論的熱門話題。有人面試題是有關“目標”,有的關於“方法”,有的關於“演算法”,有的關於“基礎”。曾經以為基礎面試十分重要,但是現在不這麼看了。在工作中基礎的確是重要的,但是在面試過程中,它必須具有區分性才有意義。 但近日,一位程
分享知識-快樂自己:Mybatis快取機制
論快取機制: 1):mybatis 提供了快取機制減輕資料庫壓力,提高資料庫效能。 2):mybatis 的快取分為兩級:一級快取、二級快取 3):一級快取是SqlSession級別的快取,快取的資料只在SqlSession內有效。 4):二級快取是mapper級別的快取,同一個
前幾天我去上海寶山面試(tianyi科技),面試官問了一個問題
問題:自己是否可以定義一個集合使其支援增強for迴圈,可以請寫出,不可以請說明理由。 當時不知道,哎,太弱了! 答案: 可以,增強for迴圈不過是Java一個語法糖 還有其他語法糖, 比如泛型中的型別擦除,自動拆箱與裝箱,邊長引數,增強for迴圈,內部類與列舉類 增強for迴圈,只要你的
軟體測試面試,面試官最想聽到的回答是什麼樣的?
其實早就計劃寫一篇這樣的文章,但是已經不求職很長一段時間了,怕我總結的內容會影響到大家,這兩天正好面試了幾個測試,再加上和朋友碰的時候總結了一點東西,想想還是寫下來分享給大家吧,希望能對正在找工作的你有所幫助 先從兩個方面說一下吧,一是普通的面試技巧方面,再從專案方面說明一下 個人介紹 當你面試測試時最
Mybatis 快取機制
快取 查詢快取主要是為了提高查詢訪問速度,即當用戶執行一次查詢後,會將該資料結果放到快取中,當下次再執行此查詢時就不會訪問資料庫了而是直接從快取中獲取該資料。 如果在快取中找到了資料那叫做命中。 一級快取 MyBatis的一級查詢快取(也叫作本地快取)是基於org.ap
面試被當成菜鳥,程式設計師:當場摘帽子,面試官:明天來上班!
很多求職者,都有過面試的經歷,這個過程很讓人煎熬,因為面試前需要做很多準備,比如修改簡歷、準備面試內容,甚至還要思考面試時要怎麼穿著才得體。 雖然說光看外表並不能客觀的反映一個人的真實能力,但是面試官也會通過求職者的外形和裝扮來判斷他們的經驗和閱歷。 學習web前端找工作這裡推
mybatis快取機制詳解
mybatis提供了快取機制減輕資料庫壓力,提高資料庫效能 mybatis的快取分為兩級:一級快取、二級快取 一級快取是SqlSession級別的快取,快取的資料只在SqlSession內有效(快取資料為執行緒私有) 二級快取是mapper級別的快取,同一個name
《深入理解mybatis原理》 MyBatis快取機制的設計與實現
本文主要講解MyBatis非常棒的快取機制的設計原理,給讀者們介紹一下MyBatis的快取機制的輪廓,然後會分別針對快取機制中的方方面面展開討論。 MyBatis將資料快取設計成兩級結構,分為一級快取、二級快取: &nb
MyBatis 快取機制-【009】
開啟二級快取配置:<setting name=“chcheEnabled” value=“true”>(value=false關閉二級快取,不會關閉一級快取。) 去mapper.xml中配置使用二級快取:<cache></cache> POJO需要實現序列化介面 每個se
程式設計師面試稱自己“理想就是不上班”,面試官:這樣能收嗎?
在求職過程中,作為求職者在回答面試官提出的所有問題時都需要“實話實說”嗎?就有一名領導在面試一個應屆程式設計師時,本來覺得其能力很好準備收了。最後快結束的時候隨口問了一句現在的工作是不是他理想的工作?沒想到這名程式設計師很直接的回答稱“我上班就是為了掙錢,不想談理想,我的理想就是不上班!”求職者這樣
直擊程式設計師面試現場:百度面試官都問了我些啥?
今天小編逛論壇的時候看見一位大牛程式設計師拿到了百度的offer,而現在群裡的小夥伴正好都在到處面試想找一份好點的工作,這位程式設計師也很無私的把他面試的經過以及面試的問題寫出來了,雖然,這些題目確實比較難,對於一個新人來講,很多知識面都是暫時接觸不到,接觸到也很難理解的,但是,小編還是希望能
Mybatis快取機制及mybatis的各個組成部分
Mybatis 一級快取: 基於PerpetualCache 的 HashMap本地快取,其儲存作用域為 Session,當 Session flush 或 close 之後,該Session中的所有 Cache 就將清空。 2. 二級快取與一級快取其機制相同,預設也是採用 Perpetual
大專程式設計師面試簡歷當場被撕,面試官:大專生我們不收
每個人都有過求職的經歷,這個過程也是很痛苦的,因為求職不可能會讓你一帆風順的,經常會使你碰壁。即便是被拒絕了之後,想想自己的哪些方面的不足,在下一次面試的時候做好準備,也沒什麼抱怨的。但是總有一些面試官的做法卻讓人感到憤怒。 就有一名程式設計師發帖講述了自己最近的一次面試被面試官撕掉簡歷的