你們好,這一篇文章是MyBatis系列的最後一篇文章,前面兩篇文章:手把手帶你閱讀Mybatis源碼(一)構造篇 和 手把手帶你閱讀Mybatis源碼(二)執行篇,主要說明了MyBatis是如何將咱們的xml配置文件構建爲其內部的Configuration對象和MappedStatement對象的,而後在第二篇咱們說了構建完成後MyBatis是如何一步一步地執行咱們的SQL語句而且對結果集進行封裝的。html
那麼這篇做爲MyBatis系列的最後一篇,天然是要來聊聊MyBatis中的一個不可忽視的功能,一級緩存和二級緩存。java
雖然這篇說的是MyBatis的緩存,可是我但願正在學習計算機的小夥伴即便尚未使用過MyBatis框架也能看明白今天這篇文章。sql
緩存是什麼?我來講說我的的理解,最後再上比較官方的概念。數據庫
緩存(Cache),顧名思義,有臨時存儲的意思。計算機中的緩存,咱們能夠直接理解爲,存儲在內存中的數據的容器,這與物理存儲是有差異的,因爲內存的讀寫速度比物理存儲高出幾個數量級,因此程序直接從內存中取數據和從物理硬盤中取數據的效率是不一樣的,因此有一些常常須要讀取的數據,設計師們一般會將其放在緩存中,以便於程序對其進行讀取。緩存
可是,緩存是有代價的,剛纔咱們說過,緩存就是在內存中的數據的容器,一條64G的內存條,一般能夠買3-4塊1T-2T的機械硬盤了,因此緩存不能無節制地使用,這樣成本會劇增,因此通常緩存中的數據都是須要頻繁查詢,可是又不常修改的數據。數據結構
而在通常業務中,查詢一般會通過以下步驟。mybatis
讀操做 --> 查詢緩存中已經存在數據 -->若是不存在則查詢數據庫,若是存在則直接查詢緩存-->數據庫查詢返回數據的同時,寫入緩存中。app
寫操做 --> 清空緩存數據 -->寫入數據庫框架
緩存流程ide
比較官方的概念:
☞ 緩存就是數據交換的緩衝區(稱做:Cache),當某一硬件要讀取數據時,會首先從緩存彙總查詢數據,有則直接執行,不存在時從內存中獲取。因爲緩存的數據比內存快的多,因此緩存的做用就是幫助硬件更快的運行。
☞ 緩存每每使用的是RAM(斷電既掉的非永久存儲),因此在用完後仍是會把文件送到硬盤等存儲器中永久存儲。電腦中最大緩存就是內存條,硬盤上也有16M或者32M的緩存。
☞ 高速緩存是用來協調CPU與主存之間存取速度的差別而設置的。通常CPU工做速度高,但內存的工做速度相對較低,爲了解決這個問題,一般使用高速緩存,高速緩存的存取速度介於CPU與主存之間。系統將一些CPU在最近幾個時間段常常訪問的內容存在高速緩存,這樣就在必定程度上緩解了因爲主存速度低形成的CPU「停工待料」的狀況。
☞ 緩存就是把一些外存上的數據保存在內存上而已,爲何保存在內存上,咱們運行的全部程序裏面的變量都是存放在內存中的,因此若是想將值放入內存上,能夠經過變量的方式存儲。在JAVA中一些緩存通常都是經過Map集合來實現的。
在說MyBatis的緩存以前,先了解一下Java中的緩存通常都是怎麼實現的,咱們一般會使用Java中的Map,來實現緩存,因此在以後的緩存這個概念,就能夠把它直接理解爲一個Map,存的就是鍵值對。
MyBatis中的一級緩存,是默認開啓且沒法關閉的,一級緩存默認的做用域是一個SqlSession,解釋一下,就是當SqlSession被構建了以後,緩存就存在了,只要這個SqlSession不關閉,這個緩存就會一直存在,換言之,只要SqlSession不關閉,那麼這個SqlSession處理的同一條SQL就不會被調用兩次,只有當會話結束了以後,這個緩存纔會一併被釋放。
雖然說咱們不能關閉一級緩存,可是做用域是能夠修改的,好比能夠修改成某個Mapper。
一級緩存的生命週期:
一、若是SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用。
二、若是SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,可是該對象仍可以使用。
三、SqlSession中執行了任何一個update操做(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,可是該對象能夠繼續使用。
節選自:https://www.cnblogs.com/happyflyingpig/p/7739749.html
MyBatis一級緩存簡單示意圖
MyBatis的二級緩存是默認關閉的,若是要開啓有兩種方式:
1.在mybatis-config.xml中加入以下配置片斷
<!-- 全局配置參數,須要時再設置 -->
<settings>
<!-- 開啓二級緩存 默認值爲true -->
<setting name="cacheEnabled" value="true"/>
</settings>
2.在mapper.xml中開啓
<!--開啓本mapper的namespace下的二級緩存-->
<!--
eviction:表明的是緩存回收策略,目前MyBatis提供如下策略。
(1) LRU,最近最少使用的,一處最長時間不用的對象
(2) FIFO,先進先出,按對象進入緩存的順序來移除他們
(3) SOFT,軟引用,移除基於垃圾回收器狀態和軟引用規則的對象
(4) WEAK,弱引用,更積極的移除基於垃圾收集器狀態和弱引用規則的對象。
這裏採用的是LRU, 移除最長時間不用的對形象
flushInterval:刷新間隔時間,單位爲毫秒,若是你不配置它,那麼當
SQL被執行的時候纔會去刷新緩存。
size:引用數目,一個正整數,表明緩存最多能夠存儲多少個對象,不宜設置過大。設置過大會致使內存溢出。
這裏配置的是1024個對象
readOnly:只讀,意味着緩存數據只能讀取而不能修改,這樣設置的好處是咱們能夠快速讀取緩存,缺點是咱們沒有
辦法修改緩存,他的默認值是false,不容許咱們修改
-->
<cache eviction="回收策略" type="緩存類"/>
二級緩存的做用域與一級緩存不一樣,一級緩存的做用域是一個SqlSession,可是二級緩存的做用域是一個namespace,什麼意思呢,你能夠把它理解爲一個mapper,在這個mapper中操做的全部SqlSession均可以共享這個二級緩存。可是假設有兩條相同的SQL,寫在不一樣的namespace下,那這個SQL就會被執行兩次,而且產生兩份value相同的緩存。
依舊是用前兩篇的測試用例,咱們從源碼的角度看看緩存是如何執行的。
public static void main(String[] args) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//從調用者角度來說 與數據庫打交道的對象 SqlSession
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","2121");
//執行這個方法實際上會走到invoke
System.out.println(mapper.selectAll(map));
sqlSession.close();
sqlSession.commit();
}
這裏會執行到query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//二級緩存的Cache,經過MappedStatement獲取
Cache cache = ms.getCache();
if (cache != null) {
//是否須要刷新緩存
//在<select>標籤中也能夠配置flushCache屬性來設置是否查詢前要刷新緩存,默認增刪改刷新緩存查詢不刷新
flushCacheIfRequired(ms);
//判斷這個mapper是否開啓了二級緩存
if (ms.isUseCache() && resultHandler == null) {
//無論
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//先從緩存拿
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//若是緩存等於空,那麼查詢一級緩存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查詢完畢後將數據放入二級緩存
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回
return list;
}
}
//若是二級緩存爲null,那麼直接查詢一級緩存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
能夠看到首先MyBatis在查詢數據時會先看看這個mapper是否開啓了二級緩存,若是開啓了,會先查詢二級緩存,若是緩存中存在咱們須要的數據,那麼直接就從緩存返回數據,若是不存在,則繼續往下走查詢邏輯。
接着往下走,若是二級緩存不存在,那麼就直接查詢數據了嗎?答案是否認的,二級緩存若是不存在,MyBatis會再查詢一次一級緩存,接着往下看。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//查詢一級緩存(localCache)
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的邏輯
* //先往緩存中put一個佔位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//往一級緩存中put真實數據
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
*/
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一級緩存和二級緩存的查詢邏輯其實差很少,都是先查詢緩存,若是沒有則進行下一步查詢,只不過一級緩存中若是沒有結果,那麼就直接查詢數據庫,而後回寫一級緩存。
講到這裏其實一級緩存和二級緩存的執行流程就說完了,緩存的邏輯其實都差很少,MyBatis的緩存是先查詢一級緩存再查詢二級緩存。
可是文章到這裏並無結束,還有一些緩存相關的問題能夠聊。
不知道這個問題你們有沒有想過,假設有這麼一個場景,這裏用二級緩存舉例,由於二級緩存是跨事務的。
假設咱們在查詢以前開啓了事務,而且進行數據庫操做:
1.往數據庫中插入一條數據(INSERT)
2.在同一個事務內查詢數據(SELECT)
3.提交事務(COMMIT)
4.提交事務失敗(ROLLBACK)
咱們來分析一下這個場景,首先SqlSession先執行了一個INSERT操做,很顯然,在咱們剛纔分析的邏輯基礎上,此時緩存必定會被清空,而後在同一個事務下查詢數據,數據又從數據庫中被加載到了緩存中,此時提交事務,而後事務提交失敗了。
考慮一下此時會出現什麼狀況,相信已經有人想到了,事務提交失敗以後,事務會進行回滾,那麼執行INSERT插入的這條數據就被回滾了,可是咱們在插入以後進行了一次查詢,這個數據已經放到了緩存中,下一次查詢必然是直接查詢緩存而不會再去查詢數據庫了,但是此時緩存和數據庫之間已經存在了數據不一致的問題。
問題的根本緣由就在於,數據庫提交事務失敗了能夠進行回滾,可是緩存不能進行回滾。
咱們來看看MyBatis是如何解決這個問題的。
這個類是MyBatis用於緩存事務管理的類,咱們能夠看看其數據結構。
public class TransactionalCacheManager {
//事務緩存
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCacheManager中封裝了一個Map,用於將事務緩存對象緩存起來,這個Map的Key是咱們的二級緩存對象,而Value是一個叫作TransactionalCache,顧名思義,這個緩存就是事務緩存,咱們來看看其內部的實現。
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//真實緩存對象
private final Cache delegate;
//是否須要清空提交空間的標識
private boolean clearOnCommit;
//全部待提交的緩存
private final Map<Object, Object> entriesToAddOnCommit;
//未命中的緩存集合,防止擊穿緩存,而且若是查詢到的數據爲null,說明要經過數據庫查詢,有可能存在數據不一致,都記錄到這個地方
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
//若是取出的是空,那麼放到未命中緩存,而且在查詢數據庫以後putObject中將本應該放到真實緩存中的鍵值對放到待提交事務緩存
entriesMissedInCache.add(key);
}
//若是不爲空
// issue #146
//查看緩存清空標識是否爲false,若是事務提交了就爲true,事務提交了會更新緩存,因此返回null。
if (clearOnCommit) {
return null;
} else {
//若是事務沒有提交,那麼返回原先緩存中的數據,
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//若是返回的數據爲null,那麼有可能到數據庫查詢,查詢到的數據先放置到待提交事務的緩存中
//原本應該put到緩存中,如今put到待提交事務的緩存中去。
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
//若是事務提交了,那麼將清空緩存提交標識設置爲true
clearOnCommit = true;
//清空entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
//若是爲true,那麼就清空緩存。
delegate.clear();
}
//把本地緩存刷新到真實緩存。
flushPendingEntries();
//而後將全部值復位。
reset();
}
public void rollback() {
//事務回滾
unlockMissedEntries();
reset();
}
private void reset() {
//復位操做。
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
//遍歷事務管理器中待提交的緩存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//寫入到真實的緩存中。
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//把未命中的一塊兒put
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
//清空真實緩存區中未命中的緩存。
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
在TransactionalCache中有一個真實緩存對象Cache,這個真實緩存對象就是咱們真正的二級緩存,還有一個 entriesToAddOnCommit,這個Map對象中存放的是全部待提交事務的緩存。
咱們在二級緩存執行的代碼中,看到在緩存中get或者put結果時,都是叫tcm的對象調用了getObject()方法和putObject()方法,這個對象實際上就是TransactionalCacheManager的實體對象,而這個對象其實是調用了TransactionalCache的方法,咱們來看看這兩個方法是如何實現的。
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
//若是取出的是空,那麼放到未命中緩存,而且在查詢數據庫以後putObject中將本應該放到真實緩存中的鍵值對放到待提交事務緩存
entriesMissedInCache.add(key);
}
//若是不爲空
// issue #146
//查看緩存清空標識是否爲false,若是事務提交了就爲true,事務提交了會更新緩存,因此返回null。
if (clearOnCommit) {
return null;
} else {
//若是事務沒有提交,那麼返回原先緩存中的數據,
return object;
}
}
@Override
public void putObject(Object key, Object object) {
//若是返回的數據爲null,那麼有可能到數據庫查詢,查詢到的數據先放置到待提交事務的緩存中
//原本應該put到緩存中,如今put到待提交事務的緩存中去。
entriesToAddOnCommit.put(key, object);
}
在getObject()方法中存在兩個分支:
若是發現緩存中取出的數據爲null,那麼會把這個key放到entriesMissedInCache中,這個對象的主要做用就是將咱們未命中的key全都保存下來,防止緩存被擊穿,而且當咱們在緩存中沒法查詢到數據,那麼就有可能到一級緩存和數據庫中查詢,那麼查詢事後會調用putObject()方法,這個方法本應該將咱們查詢到的數據put到真是緩存中,可是如今因爲存在事務,因此暫時先放到entriesToAddOnCommit中。
若是發現緩存中取出的數據不爲null,那麼會查看事務提交標識(clearOnCommit)是否爲true,若是爲true,表明事務已經提交了,以後緩存會被清空,因此返回null,若是爲false,那麼因爲事務尚未被提交,因此返回當前緩存中存的數據。
那麼當事務提交成功或提交失敗,又會是什麼情況呢?不妨看看commit和rollback方法。
public void commit() {
if (clearOnCommit) {
//若是爲true,那麼就清空緩存。
delegate.clear();
}
//把本地緩存刷新到真實緩存。
flushPendingEntries();
//而後將全部值復位。
reset();
}
public void rollback() {
//事務回滾
unlockMissedEntries();
reset();
}
先分析事務提交成功的狀況,若是事務正常提交了,那麼會有這麼幾步操做:
清空真實緩存。
將本地緩存(未提交的事務緩存 entriesToAddOnCommit)刷新到真實緩存。
將全部值復位。
咱們來看看代碼是如何實現的:
private void flushPendingEntries() {
//遍歷事務管理器中待提交的緩存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//寫入到真實的緩存中。
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
//把未命中的一塊兒put
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
//復位操做。
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
public void clear() {
//若是事務提交了,那麼將清空緩存提交標識設置爲true
clearOnCommit = true;
//清空事務提交緩存
entriesToAddOnCommit.clear();
}
清空真實緩存就不說了,就是Map調用clear方法,清空全部的鍵值對。
將未提交事務緩存刷新到真實緩存,首先會遍歷entriesToAddOnCommit,而後調用真實緩存的putObject方法,將entriesToAddOnCommit中的鍵值對put到真實緩存中,這步完成後,還會將未命中緩存中的數據一塊兒put進去,值設置爲null。
最後進行復位,將提交事務標識設爲false,未命中緩存、未提交事務緩存中的全部數據全都清空。
若是事務沒有正常提交,那麼就會發生回滾,再來看看回滾是什麼流程:
清空真實緩存中未命中的緩存。
將全部值復位
public void rollback() {
//事務回滾
unlockMissedEntries();
reset();
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
//清空真實緩存區中未命中的緩存。
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
因爲凡是在緩存中未命中的key,都會被記錄到entriesMissedInCache這個緩存中,因此這個緩存中包含了全部查詢數據庫的key,因此最終只須要在真實緩存中把這部分key和對應的value給刪除便可。
簡而言之,緩存事務的控制主要是經過TransactionalCacheManager控制TransactionCache完成的,關鍵就在於TransactionCache中的entriesToAddCommit和entriesMissedInCache這兩個對象,entriesToAddCommit在事務開啓到提交期間做爲真實緩存的替代品,將從數據庫中查詢到的數據先放到這個Map中,待事務提交後,再將這個對象中的數據刷新到真實緩存中,若是事務提交失敗了,則清空這個緩存中的數據便可,並不會影響到真實的緩存。
entriesMissedInCache主要是用來保存在查詢過程當中在緩存中沒有命中的key,因爲沒有命中,說明須要到數據庫中查詢,那麼查詢事後會保存到entriesToAddCommit中,那麼假設在事務提交過程當中失敗了,而此時entriesToAddCommit的數據又都刷新到緩存中了,那麼此時調用rollback就會經過entriesMissedInCache中保存的key,來清理真實緩存,這樣就能夠保證在事務中緩存數據與數據庫的數據保持一致。
緩存事務
因爲二級緩存的影響範圍不是SqlSession而是namespace,因此二級緩存會在你的應用啓動時一直存在直到應用關閉,因此二級緩存中不能存在隨着時間數據量愈來愈大的數據,這樣有可能會形成內存空間被佔滿。
因爲二級緩存的做用域爲namespace,那麼就能夠假設這麼一個場景,有兩個namespace操做一張表,第一個namespace查詢該表並回寫到內存中,第二個namespace往表中插一條數據,那麼第一個namespace的二級緩存是不會清空這個緩存的內容的,在下一次查詢中,還會經過緩存去查詢,這樣會形成數據的不一致。
因此當項目裏有多個命名空間操做同一張表的時候,最好不要用二級緩存,或者使用二級緩存時避免用兩個namespace操做一張表。
一級緩存的做用域是SqlSession,而使用者能夠自定義SqlSession何時出現何時銷燬,在這段期間一級緩存都是存在的。
當使用者調用close()方法以後,就會銷燬一級緩存。
可是,咱們在和Spring整合以後,Spring幫咱們跳過了SqlSessionFactory這一步,咱們能夠直接調用Mapper,致使在操做完數據庫以後,Spring就將SqlSession就銷燬了,一級緩存就隨之銷燬了,因此一級緩存就失效了。
那麼怎麼能讓緩存生效呢?
開啓事務,由於一旦開啓事務,Spring就不會在執行完SQL以後就銷燬SqlSession,由於SqlSession一旦關閉,事務就沒了,一旦咱們開啓事務,在事務期間內,緩存會一直存在。
使用二級緩存。
Hello world.
原文出處:https://www.cnblogs.com/javazhiyin/p/12357397.html