在我們平時的業務開發中,經常會使用“半自動化”的ORM框架Mybatis解決程式對資料庫操作問題。MyBatis是一個Java持久化框架,它透過XML描述符或註解把物件與儲存過程或SQL語句關聯起來。MyBatis是在Apache許可證2。0下分發的自由軟體,是iBATIS 3。0的分支版本。2001年開始開發的,是“internet”和“abtis(障礙物)”兩個單詞的組合。2004年捐贈給Apache,2010年更名為MyBatis。
對於MyBatis在java程式中的使用想必大家一定都比較清楚了,這裡主要說說它的工作流程、架構分層與模組劃分以及快取機制。
一、MyBatis的工作流程
1。1 解析配置檔案(Configuration)
mybatis啟動的時候需要解析配置檔案,包括全域性配置檔案和對映器配置檔案,我們會把它們解析成一個Configuration物件。它包含了控制mybatis的行為以及對資料庫下達的指令(SQL操作)。
1。2 提供操作介面(SqlSession)
應用程式與資料庫進行連線是透過
SqlSession
物件完成的,如果需要獲取一個會話,則需要透過會話工廠
SqlSessionFactory
介面來獲取。
透過建造者模式
SqlSessionFactoryBuilder
來建立一個工廠類,它包含所有配置檔案的配置資訊。
SqlSession
只是提供了一個介面,它還不是真正的操作資料庫的SQL執行物件。
1。3 執行SQL操作
Executor
介面用來封裝對資料庫的操作。呼叫其中query和update介面會建立一系列的物件,來處理引數、執行SQL、處理結果集,把它簡化成一個物件介面就是
StatementHandler
。
簡要的畫一下MyBatis的工作流程圖:
二、MyBatis的架構分層與模組劃分
我們開啟Mybatis的package,發現類似下面的結構:
按照不同的功能職責,也可以分成不同的工作層次。
三、MyBatis的快取
3。1 快取體系結構
Mybatis快取的預設實現是
PerpetualCache
類,它是基於HashMap實現的。
PerpetualCache
在Mybatis是基礎快取,但是快取有額外的功能,比如策略回收、日誌記錄、定時重新整理等等,如果需要使用這些功能,那麼需要在基礎快取的基礎上進行新增,需要的時候新增,不需要即可不用新增。在快取cache包下,有很多裝飾器模式的類實現了Cache介面,透過這些實現類可以實現很多快取額外的功能。
所有的快取實現總體上可以分為三大類:基本快取、淘汰演算法快取、裝飾器快取。
3。2 一級快取(Local Cache)
Mybatis的一級快取是存放在會話(
SqlSession
)層面的,一級快取是預設開啟的,不需要額外的配置,關閉的話設定
localCacheScope
的值為
STATEMENT
。原始碼的位置在
BaseExecutor
中,如下圖:
如果需要在同一個會話共享一級快取的話,那麼最好的辦法是在SqlSession內建立會話物件,讓其成為SqlSession的一個屬性,這樣的話就很方便的操作一級快取了。在同一個會話裡多次執行相同的SQL語句,會直接從記憶體拿到快取的結果集,不會再去資料庫進行操作。如果在不同的會話中,即使SQL語句一模一樣,也不會使用一級快取的。
一級快取的驗證方式
判斷是否命中快取?如果第二次傳送SQL並且到資料庫中執行,則說明沒有命中快取;如果直接列印物件,則說明是從記憶體中獲取到的結果。
測試一級快取需要先關閉二級快取,將
LocalCacheScope
設定為
SESSION
。
public void testCache() throws IOException { String resource = “mybatis-config。xml”; InputStream inputStream = Resources。getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()。build(inputStream); SqlSession session1 = sqlSessionFactory。openSession(); SqlSession session2 = sqlSessionFactory。openSession(); try { //在同一個session中共享 BlogMapper mapper0 = session1。getMapper(BlogMapper。class); BlogMapper mapper1 = session1。getMapper(BlogMapper。class); Blog blog = mapper0。selectBlogById(1); System。out。println(blog); System。out。println(“第二次查詢,相同會話,獲取到快取了嗎?”); System。out。println(mapper1。selectBlogById(1)); //不同的session不能共享 System。out。println(“第三次查詢,不同會話,獲取到快取了嗎?”); BlogMapper mapper2 = session2。getMapper(BlogMapper。class); System。out。println(mapper2。selectBlogById(1)); } finally { session1。close(); }}
一級快取在什麼時候被清空失效的呢?
在同一個session中update(包括delete)會導致一級快取被清空。
public void testCacheInvalid() throws IOException { String resource = “mybatis-config。xml”; InputStream inputStream = Resources。getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()。build(inputStream); SqlSession session = sqlSessionFactory。openSession(); try { BlogMapper mapper = session。getMapper(BlogMapper。class); System。out。println(mapper。selectBlogById(1)); Blog blog = new Blog(); blog。setBid(1); blog。setName(“after modified 666”); mapper。updateByPrimaryKey(blog); session。commit(); // 相同會話執行了更新操作,快取是否被清空? System。out。println(“在[同一個會話]執行更新操作之後,是否命中快取?”); System。out。println(mapper。selectBlogById(1)); } finally { session。close(); }}
一級快取的工作範圍是一個session中,如果跨session會出現什麼問題呢?
如果其它的session更新了資料,會導致讀取到過時的資料(一級快取不能跨session共享)
public void testDirtyRead() throws IOException { String resource = “mybatis-config。xml”; InputStream inputStream = Resources。getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()。build(inputStream); SqlSession session1 = sqlSessionFactory。openSession(); SqlSession session2 = sqlSessionFactory。openSession(); try { BlogMapper mapper1 = session1。getMapper(BlogMapper。class); System。out。println(mapper1。selectBlogById(1)); // 會話2更新了資料,會話2的一級快取更新 Blog blog = new Blog(); blog。setBid(1); blog。setName(“after modified 333333333333333333”); BlogMapper mapper2 = session2。getMapper(BlogMapper。class); mapper2。updateByPrimaryKey(blog); session2。commit(); // 其他會話更新了資料,本會話的一級快取還在麼? System。out。println(“會話1查到最新的資料了嗎?”); System。out。println(mapper1。selectBlogById(1)); } finally { session1。close(); session2。close(); }}
一級快取的不足之處
一級快取不能跨會話共享,不同的會話之間對於相同的資料可能有不同的快取。在分散式環境(多會話)下,會存在查詢到過時的資料的情況。如果有解決這個問題,那麼需要引進工作範圍更為廣發的二級快取。
3。3 二級快取
二級快取的生命週期和應用同步,它是用來解決一級快取不能跨會話共享資料的問題,範圍是namespace級別的,可以被多個會話共享(只要是同一個介面的相同方法,都可以進行共享)。
二級快取的流程圖:
一級快取是預設開始的,二級快取如何開啟呢? 1、在mybatis-config。xml中配置(預設是true)
<!—— 控制全域性快取(二級快取),預設 true——>
只要沒有顯式地設定cacheEnabled為false,都會使用CachingExector裝飾基本的執行器(SIMPLE、REUSE、BATCH)。
二級快取總是預設開啟的,但是每個Mapper的二級開關是預設關閉的。
2、在Mapper中配置cache標籤
<!—— 宣告這個namespace使用二級快取 ——>
Cache屬性詳解:
預設的回收記憶體策略是 LRU。可用的記憶體回收策略有:
LRU – 最近最少使用:移除最長時間不被使用的物件。
FIFO – 先進先出:按物件進入快取的順序來移除它們。
SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除物件。
WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除物件。
Mapper。xml 配置了cache之後,select()會被快取。update()、delete()、insert()會重新整理快取。:如果cacheEnabled=true,Mapper。xml 沒有配置標籤,還有二級快取嗎?(沒有)還會出現CachingExecutor 包裝物件嗎?(會)
只要cacheEnabled=true基本執行器就會被裝飾。有沒有配置cache,決定了在啟動的時候會不會建立這個mapper的Cache物件,只是最終會影響到CachingExecutorquery 方法裡面的判斷。如果某些查詢方法對資料的實時性要求很高,不需要二級快取,怎麼辦?我們可以在單個Statement ID 上顯式關閉二級快取(預設是true):
二級快取的驗證方式
1、事務不提交,二級快取會寫入嗎?
public void testCache() throws IOException { String resource = “mybatis-config。xml”; InputStream inputStream = Resources。getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()。build(inputStream); SqlSession session1 = sqlSessionFactory。openSession(); SqlSession session2 = sqlSessionFactory。openSession(); try { BlogMapper mapper1 = session1。getMapper(BlogMapper。class); System。out。println(mapper1。selectBlogById(1)); // 事務不提交的情況下,二級快取會寫入嗎?顯然不會,為什麼呢? session1。commit(); System。out。println(“第二次查詢”); BlogMapper mapper2 = session2。getMapper(BlogMapper。class); System。out。println(mapper2。selectBlogById(1)); } finally { session1。close(); }}
為什麼事務不提交,二級快取不生效呢?
因為二級快取使用
TransactionalCacheManager
(TCM)來管理,最後又呼叫了TransactionalCache 的getObject()、putObject和commit()方法,TransactionalCache裡面又持有了真正的Cache物件,比如是經過層層裝飾的
PerpetualCache
。在putObject 的時候,只是新增到了entriesToAddOnCommit裡面,
只有它的commit()方法被呼叫的時候才會呼叫flushPendingEntries()真正寫入快取
。它就是在
DefaultSqlSession
呼叫
commit
()的時候被呼叫的。
public void commit() { if (clearOnCommit) { delegate。clear(); } // 真正寫入二級快取 flushPendingEntries(); reset();}
private void flushPendingEntries() { for (Map。Entry
在其它的會話中執行增刪改操作,驗證快取被重新整理
public void testCacheInvalid() throws IOException { String resource = “mybatis-config。xml”; InputStream inputStream = Resources。getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()。build(inputStream); SqlSession session1 = sqlSessionFactory。openSession(); SqlSession session2 = sqlSessionFactory。openSession(); SqlSession session3 = sqlSessionFactory。openSession(); try { BlogMapper mapper1 = session1。getMapper(BlogMapper。class); BlogMapper mapper2 = session2。getMapper(BlogMapper。class); BlogMapper mapper3 = session3。getMapper(BlogMapper。class); System。out。println(mapper1。selectBlogById(1)); session1。commit(); // 是否命中二級快取 System。out。println(“是否命中二級快取?”); System。out。println(mapper2。selectBlogById(1)); Blog blog = new Blog(); blog。setBid(1); blog。setName(“2020年5月13日15:03:38”); mapper3。updateByPrimaryKey(blog); session3。commit(); System。out。println(“更新後再次查詢,是否命中二級快取?”); // 在其他會話中執行了更新操作,二級快取是否被清空? System。out。println(mapper2。selectBlogById(1)); } finally { session1。close(); session2。close(); session3。close(); }}
為什麼增刪改操作會清空快取?
在
CachingExecutor
的update()方法裡面會呼叫flushCacheIfRequired(ms),
isFlushCacheRequired
就是從標籤裡面渠道的flushCache 的值。而增刪改操作的
flushCache
屬性預設為true。
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms。getCache(); // 增刪改查的標籤上有屬性:flushCache=“true” (select語句預設是false) // 一級二級快取都會被清理 if (cache != null && ms。isFlushCacheRequired()) { tcm。clear(cache); }}
什麼時候開啟二級快取呢?
一級快取預設是開啟的,二級快取需要配置才可以開啟。那麼我們必須思考一個問題,在什麼情況下才有必要去開啟二級快取?
因為所有的增刪改都會重新整理二級快取,導致二級快取失效,所以適合在查詢為主的應用中使用,比如歷史交易、歷史訂單的查詢。否則快取就失去了意義。如果多個namespace 中有針對於同一個表的操作,如果在一個namespace中重新整理了快取,另一個namespace中沒有重新整理,就會出現讀到髒資料的情況。所以,推薦在一個Mapper 裡面只操作單表的情況使用。如果要讓多個namespace共享一個二級快取,應該怎麼做?跨namespace的快取共享的問題,可以使用cache-ref配置來解決:
cache-ref 代表引用別的名稱空間的Cache配置,兩個名稱空間的操作使用的是同一個Cache。在關聯的表比較少,或者按照業務可以對錶進行分組的時候可以使用。
注意:在這種情況下,多個Mapper的操作都會引起快取重新整理,快取的意義已經不大了
第三方快取做二級快取
除了MyBatis 自帶的二級快取之外,我們也可以透過實現Cache 介面來自定義二級快取。MyBatis官方提供了一些第三方快取整合方式,比如ehcache 和redis:
https://github。com/mybatis/redis-cache
當然,我們也可以使用獨立的快取服務,不使用MyBatis 自帶的二級快取。
pom檔案引入的依賴:
mapper。xml配置檔案的內容:
<!—— 使用Redis作為二級快取 ——>
redis。properties配置檔案內容:
host=localhostport=6379connectionTimeout=5000soTimeout=5000database=0
當然,我們在分散式的環境中,也可以使用獨立的快取服務,不使用MyBatis自帶的二級快取。