www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁 > 公眾號精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]一、MyBatis緩存緩存就是內(nèi)存中的數(shù)據(jù),常常來自對數(shù)據(jù)庫查詢結(jié)果的保存。使用緩存,我們可以避免頻繁與數(shù)據(jù)庫進行交互,從而提高響應(yīng)速度。MyBatis也提供了對緩存的支持,分為一級緩存和二級緩存,來看下下面這張圖:一級緩存是SqlSession級別的緩存。在操作數(shù)據(jù)庫時需要構(gòu)造...



一、MyBatis 緩存

緩存就是內(nèi)存中的數(shù)據(jù),常常來自對數(shù)據(jù)庫查詢結(jié)果的保存。使用緩存,我們可以避免頻繁與數(shù)據(jù)庫進行交互,從而提高響應(yīng)速度。

MyBatis 也提供了對緩存的支持,分為一級緩存和二級緩存,來看下下面這張圖:

深入淺出?MyBatis?的一級、二級緩存機制

一級緩存是 SqlSession 級別的緩存。在操作數(shù)據(jù)庫時需要構(gòu)造 SqlSession 對象,在對象中有一個數(shù)據(jù)結(jié)構(gòu)(HashMap)用于存儲緩存數(shù)據(jù)。不同的是 SqlSession 之間的緩存數(shù)據(jù)區(qū)(HashMap)是互相不影響。二級緩存是 Mapper 級別的緩存,多個 SqlSession 去操作同一個 Mapper 的 sql 語句,多個 SqlSession 可以共用二級緩存,二級緩存是跨 SqlSession 的。

相信大家看完這張圖和解釋心里應(yīng)該有個底了吧,這對后面分析 MyBatis 的一級、二級緩存機制很有幫助,那話不多說,我們直接進入主題了。

二、一級緩存

2.1 內(nèi)部結(jié)構(gòu)

在我們的應(yīng)用運行期間,我們可能在一次數(shù)據(jù)庫會話中,執(zhí)行多次查詢條件相同的 SQL,要你來設(shè)計的話你會如何考慮?沒錯,加緩存,MyBatis 也是這樣去處理的,如果是相同的 SQL 語句,會優(yōu)先命中一級緩存,避免直接對數(shù)據(jù)庫進行查詢,造成數(shù)據(jù)庫的壓力,以提高性能。具體執(zhí)行過程如下圖所示:

深入淺出?MyBatis?的一級、二級緩存機制
SqlSession 是一個接口,提供了一些 CRUD 的方法,而 SqlSession 的默認(rèn)實現(xiàn)類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的默認(rèn)實現(xiàn)是 BaseExecutor 對象,每個 BaseExecutor 對象都有一個 PerpetualCache 緩存,也就是上圖的 ?Local Cache。

當(dāng)用戶發(fā)起查詢時,MyBatis 根據(jù)當(dāng)前執(zhí)行的語句生成 MappedStatement,在 Local Cache 進行查詢,如果緩存命中的話,直接返回結(jié)果給用戶,如果緩存沒有命中的話,查詢數(shù)據(jù)庫,結(jié)果寫入 Local Cache,最后返回結(jié)果給用戶。

啊,老周,關(guān)系還是有點抽象,感覺一直在套娃,沒關(guān)系,看下面這張圖你立馬豁然開朗。

深入淺出?MyBatis?的一級、二級緩存機制

2.2 一級緩存配置在 MyBatis 的配置文件中添加如下語句,就可以使用一級緩存。共有兩個選項,SESSION 或者 STATEMENT,默認(rèn)是 SESSION 級別,即在一個 MyBatis 會話中執(zhí)行的所有語句,都會共享這一個緩存。一種是 STATEMENT 級別,可以理解為緩存只對當(dāng)前執(zhí)行的這一個 Statement 有效。

STATEMENT 級別粒度更細(xì),我們上面說到,每個 SqlSession 中持有了 Executor,SqlSession 的默認(rèn)實現(xiàn)類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的默認(rèn)實現(xiàn)是 BaseExecutor 對象,每個 BaseExecutor 對象很多方法中都有傳 MappedStatement 對象。所有 STATEMENT 級別是針對 SESSION 級別粒度更細(xì)的模式。

"localCacheScope"?value="SESSION"/>

三、一級緩存實驗

下面老周通過幾組實驗來帶你了解 MyBatis 一級緩存的效果,我們首先準(zhǔn)備一張簡單的表 user,如下:

CREATE?TABLE?`user`?(
??`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT,
??`name`?varchar(64)?COLLATE?utf8_bin?DEFAULT?NULL,
??PRIMARY?KEY?(`id`)
)?ENGINE=InnoDB?AUTO_INCREMENT=2?DEFAULT?CHARSET=utf8?COLLATE=utf8_bin;
我們在測試類中加上帶有 @Before 標(biāo)注的 before 方法,省得每個單元測試方法都要重復(fù)獲取 sqlSession 以及 userMapper。

@Before
public?void?before()?throws?IOException?{
????InputStream?resourceAsStream?=?Resources.getResourceAsStream("SqlMapConfig.xml");
????sqlSessionFactory?=?new?SqlSessionFactoryBuilder().build(resourceAsStream);
????sqlSession?=?sqlSessionFactory.openSession(true);?//?自動提交事務(wù)
????userMapper?=?sqlSession.getMapper(UserMapper.class);
}
3.1 實驗1

開啟一級緩存,范圍為會話級別,調(diào)用三次 firstLevelCacheFindUserById,代碼如下所示:

@Test
public?void?firstLevelCacheFindUserById()?{
????//?第一次查詢id為1的用戶
????User?user1?=?userMapper.findUserById(1);
????//?第二次查詢id為1的用戶
????User?user2?=?userMapper.findUserById(1);

????System.out.println(user1);
????System.out.println(user2);

????System.out.println(user1?==?user2);
}
控制臺日志輸出:

深入淺出?MyBatis?的一級、二級緩存機制

我們可以看到,只有第一次真正查詢了數(shù)據(jù)庫,后續(xù)的查詢使用了一級緩存。3.2 實驗2

增加了對數(shù)據(jù)庫的修改操作,驗證在一次數(shù)據(jù)庫會話中,如果對數(shù)據(jù)庫發(fā)生了修改操作,一級緩存是否會失效。

@Test
public?void?firstLevelCacheOfUpdate()?{
????//?第一次查詢id為1的用戶
????User?user1?=?userMapper.findUserById(1);
????System.out.println(user1);

????//?更新用戶
????User?user?=?new?User();
????user.setId(2);
????user.setUsername("tom");

????System.out.println("更新了"? ?userMapper.updateUser(user)? ?"個用戶");

????//?第二次查詢id為1的用戶
????User?user2?=?userMapper.findUserById(1);
????System.out.println(user2);

????System.out.println(user1?==?user2);
}
控制臺日志輸出:

深入淺出?MyBatis?的一級、二級緩存機制

我們可以看到,在修改操作后執(zhí)行的相同查詢,查詢了數(shù)據(jù)庫,一級緩存失效。3.3 實驗3

開啟兩個 SqlSession,在 sqlSession1 中查詢數(shù)據(jù),使一級緩存生效,在 sqlSession2 中更新數(shù)據(jù)庫,驗證一級緩存只在數(shù)據(jù)庫會話內(nèi)部共享。

@Test
public?void?firstLevelCacheOfScope()?{
????SqlSession?sqlSession2?=?sqlSessionFactory.openSession(true);
????UserMapper?userMapper2?=?sqlSession2.getMapper(UserMapper.class);

????System.out.println("userMapper讀取數(shù)據(jù):?"? ?userMapper.findUserById(1));
????System.out.println("userMapper讀取數(shù)據(jù):?"? ?userMapper.findUserById(1));

????//?更新用戶
????User?user?=?new?User();
????user.setId(1);
????user.setUsername("andy");
????System.out.println("userMapper2更新了"? ?userMapper2.updateUser(user)? ?"個用戶");

????System.out.println("userMapper讀取數(shù)據(jù):?"? ?userMapper.findUserById(1));
????System.out.println("userMapper2讀取數(shù)據(jù):?"? ?userMapper2.findUserById(1));
}
控制臺日志輸出:

深入淺出?MyBatis?的一級、二級緩存機制

sqlSession2 更新了 id 為 1 的用戶的姓名,從 riemann 改為了 andy,但 session1 之后的查詢中,id 為 1 的學(xué)生的名字還是 riemann,出現(xiàn)了臟數(shù)據(jù),也證明了之前的設(shè)想,一級緩存只在數(shù)據(jù)庫會話內(nèi)部共享。

四、一級緩存工作流程以及源碼分析

4.1 工作流程

我們來看下一級緩存的時序圖:

深入淺出?MyBatis?的一級、二級緩存機制

4.2 源碼分析看完上面的整體時序流程,我相信大家基本框架了解了,接下來針對這個框架再進行源碼細(xì)化走讀。

SqlSession:對外提供了用戶和數(shù)據(jù)庫之間交互需要的所有方法,隱藏了底層的細(xì)節(jié)。默認(rèn)實現(xiàn)類是 DefaultSqlSession。

深入淺出?MyBatis?的一級、二級緩存機制

Executor:SqlSession 向用戶提供操作數(shù)據(jù)庫的方法,但和數(shù)據(jù)庫操作有關(guān)的職責(zé)都會委托給 Executor。
深入淺出?MyBatis?的一級、二級緩存機制

如下圖所示,Executor 有若干個實現(xiàn)類,為 Executor 賦予了不同的能力。
深入淺出?MyBatis?的一級、二級緩存機制

在一級緩存的源碼分析中,主要學(xué)習(xí) BaseExecutor 的內(nèi)部實現(xiàn)。BaseExecutor:BaseExecutor 是一個實現(xiàn)了 Executor 接口的抽象類,定義若干抽象方法,在執(zhí)行的時候,把具體的操作委托給子類進行執(zhí)行。

protected?abstract?int?doUpdate(MappedStatement?var1,?Object?var2)?throws?SQLException;
protected?abstract?List?doFlushStatements(boolean?var1)?throws?SQLException;
protected?abstract??List?doQuery(MappedStatement?var1,?Object?var2,?RowBounds?var3,?ResultHandler?var4,?BoundSql?var5)?throws?SQLException;
protected?abstract??Cursor?doQueryCursor(MappedStatement?var1,?Object?var2,?RowBounds?var3,?BoundSql?var4)?throws?SQLException;
在上文有提到對 Local Cache 的查詢和寫入是在 Executor 內(nèi)部完成的。在閱讀 BaseExecutor 的代碼后發(fā)現(xiàn) Local Cache 是 BaseExecutor 內(nèi)部的一個成員變量,如下代碼所示。

public?abstract?class?BaseExecutor?implements?Executor?{
????...
????protected?ConcurrentLinkedQueue?deferredLoads;
????protected?PerpetualCache?localCache;
????protected?PerpetualCache?localOutputParameterCache;
????...
}
Cache:MyBatis 中的 Cache 接口,提供了和緩存相關(guān)的最基本的操作,如下圖所示:

深入淺出?MyBatis?的一級、二級緩存機制

有若干個實現(xiàn)類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力,部分實現(xiàn)類如下圖所示:
深入淺出?MyBatis?的一級、二級緩存機制

BaseExecutor 成員變量之一的 PerpetualCache,是對 Cache 接口最基本的實現(xiàn),其實現(xiàn)非常簡單,內(nèi)部持有 HashMap,對一級緩存的操作實則是對 HashMap 的操作。如下代碼所示:
public?class?PerpetualCache?implements?Cache?{
????private?final?String?id;
????private?Map?cache?=?new?HashMap();
????...
}
調(diào)研了一下,畫出工作流程圖:

深入淺出?MyBatis?的一級、二級緩存機制

跟蹤到 PerpetualCache 中的 clear() 方法。
public?class?PerpetualCache?implements?Cache?{
????...
????private?Map?cache?=?new?HashMap();

????public?void?clear()?{
????????this.cache.clear();
????}
????...
}
也就是說一級緩存的底層數(shù)據(jù)結(jié)構(gòu)就是 HashMap。所以說 cache.clear() 其實就是 map.clear(),也就是說,緩存其實是本地存放的一個 map 對象,每一個 SqlSession 都會存放一個 map 對象的引用。

那么這個 cache 是何時創(chuàng)建的呢?

根據(jù)上面我們畫的工作流程,明顯在 Executor 執(zhí)行器,執(zhí)行器用來執(zhí)行 sql 請求,而且清除緩存的方法也在 Executor 中執(zhí)行,去查看源碼,果真在里面。

深入淺出?MyBatis?的一級、二級緩存機制

創(chuàng)建緩存 key 會經(jīng)過一系列的 update 方法,update 方法由一個 cacheKey 這個對象來執(zhí)行的。這個 update 方法最終由 updateList 的 list 來把六個值存進去,對照上面的代碼,你應(yīng)該能理解下面六個值分別是啥了吧。
深入淺出?MyBatis?的一級、二級緩存機制

這里需要關(guān)注最后一個值 this.configuration.getEnvironment().getId(),這其實就是定義在 mybatis-config.xml 中的標(biāo)簽。如下:
深入淺出?MyBatis?的一級、二級緩存機制

那么問題來了,創(chuàng)建緩存了,那具體在哪里用呢?我們一級緩存探究后,我們發(fā)現(xiàn)一級緩存更多的用于查詢操作。我們跟蹤到 query 方法:
深入淺出?MyBatis?的一級、二級緩存機制

如果查不到的話,就從數(shù)據(jù)庫查,在 queryFromDatabase 中,會對 localcache 進行寫入。在 query 方法執(zhí)行的最后,會判斷一級緩存級別是否是 STATEMENT 級別,如果是的話,就清空緩存,這也就是 STATEMENT 級別的一級緩存無法共享 localCache 的原因。代碼如下所示:

if?(configuration.getLocalCacheScope()?==?LocalCacheScope.STATEMENT)?{
????clearLocalCache();
}
在源碼分析的最后,我們確認(rèn)一下,如果是 insert/delete/update 方法,緩存就會刷新的原因。

SqlSession 的 insert 方法和 delete 方法,都會統(tǒng)一走 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 執(zhí)行。BaseExecutor 的執(zhí)行方法如下所示:

@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);
}
每次執(zhí)行 update 前都會清空 localCache。

至此,一級緩存的工作流程講解以及源碼分析完畢。

五、一級緩存小結(jié)

  • MyBatis 一級緩存的生命周期和 SqlSession 一致。

  • MyBatis 一級緩存內(nèi)部設(shè)計簡單,只是一個沒有容量限定的 HashMap,在緩存的功能性上有所欠缺。

  • MyBatis 的一級緩存最大范圍是 SqlSession 內(nèi)部,有多個 SqlSession 或者分布式的環(huán)境下,數(shù)據(jù)庫寫操作會引起臟數(shù)據(jù),建議設(shè)定緩存級別為 Statement。

六、二級緩存

在上文中提到的一級緩存中,其最大的共享范圍就是一個 SqlSession 內(nèi)部,如果多個 SqlSession 之間需要共享緩存,則需要使用到二級緩存。開啟二級緩存后,會使用 CachingExecutor 裝飾 Executor,進入一級緩存的查詢流程前,先在 CachingExecutor 進行二級緩存的查詢,具體的工作流程如下所示。

深入淺出?MyBatis?的一級、二級緩存機制

二級緩存開啟后,同一個 namespace 下的所有操作語句,都影響著同一個 Cache,即二級緩存被多個 SqlSession 共享,是一個全局的變量。當(dāng)開啟緩存后,數(shù)據(jù)的查詢執(zhí)行的流程就是 二級緩存 -> 一級緩存 -> 數(shù)據(jù)庫。

MyBatis 是默認(rèn)關(guān)閉二級緩存的,因為對于增刪改操作頻繁的話,那么二級緩存形同虛設(shè),每次都會被清空緩存。

6.1 二級緩存配置

和一級緩存默認(rèn)開啟不一樣,二級緩存需要我們手動開啟。

6.1.1 首先在全局配置文件 SqlMapConfig.xml 文件中加入如下代碼:


<settings>
????<setting?name="cacheEnabled"?value="true"/>
settings>
6.1.2 其次在 UserMapper.xml 文件中開啟二級緩存

mapper 代理模式:


<cache?/>
注解開發(fā)模式:

@CacheNamespace(implementation?=?PerpetualCache.class)?//?開啟二級緩存
public?interface?UserMapper?{
}
mapper 代理模式開啟的二級緩存是一個空標(biāo)簽,其實這里可以配置,PerpetualCache 這個類是 mybatis 默認(rèn)實現(xiàn)的二級緩存功能的類,我們不寫 type ,用 @CacheNamespace 直接默認(rèn) PerpetualCache 這個類,也可以去實現(xiàn) Cache 接口來自定義緩存。

深入淺出?MyBatis?的一級、二級緩存機制
6.2 實體類實現(xiàn) Serializable 序列化接口

深入淺出?MyBatis?的一級、二級緩存機制

開啟二級緩存后,還需要將要緩存的實體類去實現(xiàn) Serializable 序列化接口,為了將緩存數(shù)據(jù)取出執(zhí)行反序列化操作,因為二級緩存數(shù)據(jù)存儲介質(zhì)多種多樣,不一定只存在內(nèi)存中,有可能存在硬盤中,如果我們再取出這個緩存的話,就需要反序列化。所以 MyBatis 的所有 pojo 類都要去實現(xiàn) Serializable 序列化接口。

七、二級緩存實驗

7.1 實驗1

測試二級緩存與 SqlSession 無關(guān)

@Test
public?void?secondLevelCache()?{
????SqlSession?sqlSession1?=?sqlSessionFactory.openSession();
????SqlSession?sqlSession2?=?sqlSessionFactory.openSession();

????UserMapper?userMapper1?=?sqlSession1.getMapper(UserMapper.class);
????UserMapper?userMapper2?=?sqlSession2.getMapper(UserMapper.class);

????//?第一次查詢id為1的用戶
????User?user1?=?userMapper1.findUserById(1);
????sqlSession1.close();?//?清空一級緩存
????System.out.println(user1);

????//?第二次查詢id為1的用戶
????User?user2?=?userMapper2.findUserById(1);
????System.out.println(user2);

????System.out.println(user1?==?user2);
}
控制臺日志輸出:

深入淺出?MyBatis?的一級、二級緩存機制

第一次查詢時,將查詢結(jié)果放入緩存中,第二次查詢,即使 sqlSession1.close(); 清空了一級緩存,第二次查詢依然不發(fā)出 sql 語句。這里的你可能有個疑問,這里不是二級緩存了嗎?怎么 user1 與 user2 不相等?

這是因為二級緩存的是數(shù)據(jù),并不是對象。而 user1 與 user2 是兩個對象,所以地址值當(dāng)然也不想等。

7.2 實驗2

測試執(zhí)行 commit(),二級緩存數(shù)據(jù)清空。

@Test
public?void?secondLevelCacheOfUpdate()?{
????SqlSession?sqlSession1?=?sqlSessionFactory.openSession();
????SqlSession?sqlSession2?=?sqlSessionFactory.openSession();
????SqlSession?sqlSession3?=?sqlSessionFactory.openSession();

????UserMapper?userMapper1?=?sqlSession1.getMapper(UserMapper.class);
????UserMapper?userMapper2?=?sqlSession2.getMapper(UserMapper.class);
????UserMapper?userMapper3?=?sqlSession3.getMapper(UserMapper.class);

????//?第一次查詢id為1的用戶
????User?user1?=?userMapper1.findUserById(1);
????sqlSession1.close();?//?清空一級緩存

????User?user?=?new?User();
????user.setId(3);
????user.setUsername("edgar");
????userMapper3.updateUser(user);
????sqlSession3.commit();

????//?第二次查詢id為1的用戶
????User?user2?=?userMapper2.findUserById(1);
????sqlSession2.close();

????System.out.println(user1?==?user2);
}
控制臺日志輸出:

深入淺出?MyBatis?的一級、二級緩存機制

我們可以看到,在 sqlSession3 更新數(shù)據(jù)庫,并提交事務(wù)后,sqlsession2 的 UserMapper namespace 下的查詢走了數(shù)據(jù)庫,沒有走 Cache。7.3 實驗3

驗證 MyBatis 的二級緩存不適應(yīng)用于映射文件中存在多表查詢的情況。

通常我們會為每個單表創(chuàng)建單獨的映射文件,由于 MyBatis 的二級緩存是基于 namespace 的,多表查詢語句所在的 namspace 無法感應(yīng)到其他 namespace 中的語句對多表查詢中涉及的表進行的修改,引發(fā)臟數(shù)據(jù)問題。

為了解決實驗3的問題呢,可以使用 Cache ref,讓 OrderMapper 引用 UserMapper 命名空間,這樣兩個映射文件對應(yīng)的 SQL 操作都使用的是同一塊緩存了。

不過這樣做的后果是,緩存的粒度變粗了,多個 Mapper namespace 下的所有操作都會對緩存使用造成影響。

這里老周就不代碼演示了,有沒有感覺很雞肋,而且不熟用二級緩存的話,像這種多表查詢的,很容易造成臟讀數(shù)據(jù)不一致,這在線上的話是致命的。

7.4 useCache 和 flushCache

useCache 是用來設(shè)置是否禁用二級緩存的,在 statement 中設(shè)置 useCache="false",可以禁用當(dāng)前 select 語句的二級緩存,即每次都會去數(shù)據(jù)庫查詢。如下:

<select?id="findAll"?resultMap="userMap"?useCache="false">
????select?*?from?user?u?left?join?orders?o?on?u.id?=?o.uid
select>
設(shè)置 statement 配置中的 flushCache=“true” 屬性,默認(rèn)情況下為 true,即刷新緩存,一般執(zhí)行完 commit 操作都需要刷新緩存,flushCache=“true” 表示刷新緩存,這樣可以避免增刪改操作而導(dǎo)致的臟讀問題。默認(rèn)不要配置,如下:

<select?id="findAll"?resultMap="userMap"?useCache="false"?flushCache="true">
????select?*?from?user?u?left?join?orders?o?on?u.id?=?o.uid
select>

八、二級緩存源碼分析

MyBatis 二級緩存的工作流程和前文提到的一級緩存類似,只是在一級緩存處理前,用 CachingExecutor 裝飾了 BaseExecutor 的子類,在委托具體職責(zé)給 delegate 之前,實現(xiàn)了二級緩存的查詢和寫入功能,具體類關(guān)系圖如下圖所示。

深入淺出?MyBatis?的一級、二級緩存機制

源碼分析從 CachingExecutor 的 query 方法展開,首先會從 MappedStatement 中獲得在配置初始化時賦予的 Cache。
public??List?query(MappedStatement?ms,?Object?parameterObject,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
????Cache?cache?=?ms.getCache();?//?首先會從?MappedStatement?中獲得在配置初始化時賦予的?Cache
????if?(cache?!=?null)?{
????????this.flushCacheIfRequired(ms);
????????if?(ms.isUseCache()?
本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
關(guān)閉
關(guān)閉