面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

在我們平時的業務開發中,經常會使用“半自動化”的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操作)。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

1。2 提供操作介面(SqlSession)

應用程式與資料庫進行連線是透過

SqlSession

物件完成的,如果需要獲取一個會話,則需要透過會話工廠

SqlSessionFactory

介面來獲取。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

透過建造者模式

SqlSessionFactoryBuilder

來建立一個工廠類,它包含所有配置檔案的配置資訊。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

SqlSession

只是提供了一個介面,它還不是真正的操作資料庫的SQL執行物件。

1。3 執行SQL操作

Executor

介面用來封裝對資料庫的操作。呼叫其中query和update介面會建立一系列的物件,來處理引數、執行SQL、處理結果集,把它簡化成一個物件介面就是

StatementHandler

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

簡要的畫一下MyBatis的工作流程圖:

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

二、MyBatis的架構分層與模組劃分

我們開啟Mybatis的package,發現類似下面的結構:

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

按照不同的功能職責,也可以分成不同的工作層次。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

三、MyBatis的快取

3。1 快取體系結構

Mybatis快取的預設實現是

PerpetualCache

類,它是基於HashMap實現的。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

PerpetualCache

在Mybatis是基礎快取,但是快取有額外的功能,比如策略回收、日誌記錄、定時重新整理等等,如果需要使用這些功能,那麼需要在基礎快取的基礎上進行新增,需要的時候新增,不需要即可不用新增。在快取cache包下,有很多裝飾器模式的類實現了Cache介面,透過這些實現類可以實現很多快取額外的功能。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

所有的快取實現總體上可以分為三大類:基本快取、淘汰演算法快取、裝飾器快取。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

3。2 一級快取(Local Cache)

Mybatis的一級快取是存放在會話(

SqlSession

)層面的,一級快取是預設開啟的,不需要額外的配置,關閉的話設定

localCacheScope

的值為

STATEMENT

。原始碼的位置在

BaseExecutor

中,如下圖:

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

如果需要在同一個會話共享一級快取的話,那麼最好的辦法是在SqlSession內建立會話物件,讓其成為SqlSession的一個屬性,這樣的話就很方便的操作一級快取了。在同一個會話裡多次執行相同的SQL語句,會直接從記憶體拿到快取的結果集,不會再去資料庫進行操作。如果在不同的會話中,即使SQL語句一模一樣,也不會使用一級快取的。

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

一級快取的驗證方式

判斷是否命中快取?如果第二次傳送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級別的,可以被多個會話共享(只要是同一個介面的相同方法,都可以進行共享)。

二級快取的流程圖:

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

一級快取是預設開始的,二級快取如何開啟呢? 1、在mybatis-config。xml中配置(預設是true)

<!—— 控制全域性快取(二級快取),預設 true——>

只要沒有顯式地設定cacheEnabled為false,都會使用CachingExector裝飾基本的執行器(SIMPLE、REUSE、BATCH)。

二級快取總是預設開啟的,但是每個Mapper的二級開關是預設關閉的。

2、在Mapper中配置cache標籤

<!—— 宣告這個namespace使用二級快取 ——> eviction=“LRU”<!—— 快取策略 ——> flushInterval=“120000”<!—— 自動重新整理時間ms,未配置是隻有呼叫時重新整理 ——> readOnly=“false”/><!—— 預設是false(安全),改為true可讀寫時,物件必須支援序列化 ——>

Cache屬性詳解:

面試官口中的Mybatis,工作流程、架構分層模組劃分以及快取機制

預設的回收記憶體策略是 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):