mybatis 快取的使用, 看這篇就夠了
版權宣告:本文為博主原創文章,未經博主允許不得轉載。https://blog.csdn.net/weixin_37139197/article/details/82908377
快取的重要性是不言而喻的。 使用快取, 我們可以避免頻繁的與資料庫進行互動, 尤其是在查詢越多、快取命中率越高的情況下, 使用快取對效能的提高更明顯。
mybatis 也提供了對快取的支援, 分為一級快取和二級快取。 但是在預設的情況下, 只開啟一級快取(一級快取是對同一個 SqlSession 而言的)。
以下的專案是在ofollow,noindex" target="_blank">mybatis 初步使用(IDEA的Maven專案, 超詳細) 的基礎上進行。
對以下的程式碼, 你也可以從我的GitHub 中獲取相應的專案。
1 一級快取
同一個SqlSession
物件, 在引數和 SQL 完全一樣的情況先, 只執行一次 SQL 語句(如果快取沒有過期)
也就是隻有在引數和 SQL 完全一樣的情況下, 才會有這種情況。
1.1 同一個 SqlSession
@Test public void oneSqlSession() { SqlSession sqlSession = null; try { sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); // 執行第一次查詢 List<Student> students = studentMapper.selectAll(); for (int i = 0; i < students.size(); i++) { System.out.println(students.get(i)); } System.out.println("=============開始同一個 Sqlsession 的第二次查詢============"); // 同一個 sqlSession 進行第二次查詢 List<Student> stus = studentMapper.selectAll(); Assert.assertEquals(students, stus); for (int i = 0; i < stus.size(); i++) { System.out.println("stus:" + stus.get(i)); } } catch (Exception e) { e.printStackTrace(); } finally { if (sqlSession != null) { sqlSession.close(); } } }
在以上的程式碼中, 進行了兩次查詢, 使用相同的SqlSession
, 結果如下
在日誌和輸出中:
第一次查詢傳送了 SQL 語句, 後返回了結果;
第二次查詢沒有傳送 SQL 語句, 直接從記憶體中獲取了結果。
而且兩次結果輸入一致, 同時斷言兩個物件相同也通過。
1.2 不同的 SqlSession
@Test public void differSqlSession() { SqlSession sqlSession = null; SqlSession sqlSession2 = null; try { sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); // 執行第一次查詢 List<Student> students = studentMapper.selectAll(); for (int i = 0; i < students.size(); i++) { System.out.println(students.get(i)); } System.out.println("=============開始不同 Sqlsession 的第二次查詢============"); // 從新建立一個 sqlSession2 進行第二次查詢 sqlSession2 = sqlSessionFactory.openSession(); StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); List<Student> stus = studentMapper2.selectAll(); // 不相等 Assert.assertNotEquals(students, stus); for (int i = 0; i < stus.size(); i++) { System.out.println("stus:" + stus.get(i)); } } catch (Exception e) { e.printStackTrace(); } finally { if (sqlSession != null) { sqlSession.close(); } if (sqlSession2 != null) { sqlSession2.close(); } } }
在程式碼中, 分別使用sqlSession 和sqlSession2 進行了相同的查詢。
其結果如下
從日誌中可以看到兩次查詢都分別從資料庫中取出了資料。 雖然結果相同, 但兩個是不同的物件。
1.3 重新整理快取
重新整理快取是清空這個 SqlSession 的所有快取, 不單單是某個鍵。
@Test public void sameSqlSessionNoCache() { SqlSession sqlSession = null; try { sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); // 執行第一次查詢 Student student = studentMapper.selectByPrimaryKey(1); System.out.println("=============開始同一個 Sqlsession 的第二次查詢============"); // 同一個 sqlSession 進行第二次查詢 Student stu = studentMapper.selectByPrimaryKey(1); Assert.assertEquals(student, stu); } catch (Exception e) { e.printStackTrace(); } finally { if (sqlSession != null) { sqlSession.close(); } } }
如果是以上, 沒什麼不同, 結果還是第二個不發 SQL 語句。
在此, 做一些修改, 在StudentMapper.xml 中, 新增
flushCache=“true”
修改後的配置檔案如下:
<select id="selectByPrimaryKey" flushCache="true" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from student where student_id=#{id, jdbcType=INTEGER} </select>
結果如下:
第一次, 第二次都發送了 SQL 語句, 同時, 斷言兩個物件相同出錯。
1.4 總結
-
在同一個
SqlSession
中, Mybatis 會把執行的方法和引數通過演算法生成快取的鍵值, 將鍵值和結果存放在一個 Map 中, 如果後續的鍵值一樣, 則直接從 Map 中獲取資料; -
不同的
SqlSession
之間的快取是相互隔離的; -
用一個
SqlSession
, 可以通過配置使得在查詢前清空快取; -
任何的 UPDATE, INSERT, DELETE 語句都會清空快取。
2 二級快取
二級快取存在於 SqlSessionFactory 生命週期中。
2.1 配置二級快取
2.1.1 全域性開關
在 mybatis 中, 二級快取有全域性開關和分開關, 全域性開關, 在mybatis-config.xml 中如下配置:
<settings> <!--全域性地開啟或關閉配置檔案中的所有對映器已經配置的任何快取。 --> <setting name="cacheEnabled" value="true"/> </settings>
預設是為 true, 即預設開啟總開關。
2.1.2 分開關
分開關就是說在 *Mapper.xml 中開啟或關閉二級快取, 預設是不開啟的。
2.1.3 entity 實現序列化介面
public class Student implements Serializable { private static final long serialVersionUID = -4852658907724408209L; ... }
2.2 使用二級快取
@Test public void secendLevelCacheTest() { // 獲取 SqlSession 物件 SqlSession sqlSession = sqlSessionFactory.openSession(); //獲取 Mapper 物件 StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); // 使用 Mapper 介面的對應方法,查詢 id=2 的物件 Student student = studentMapper.selectByPrimaryKey(2); // 更新物件的名稱 student.setName("奶茶"); // 再次使用相同的 SqlSession 查詢id=2 的物件 Student student1 = studentMapper.selectByPrimaryKey(2); Assert.assertEquals("奶茶", student1.getName()); // 同一個 SqlSession , 此時是一級快取在作用, 兩個物件相同 Assert.assertEquals(student, student1); sqlSession.close(); SqlSession sqlSession1 = sqlSessionFactory.openSession(); StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class); Student student2 = studentMapper1.selectByPrimaryKey(2); Student student3 = studentMapper1.selectByPrimaryKey(2); // 由於我們配置的 readOnly="true", 因此後續同一個 SqlSession 的物件都不一樣 Assert.assertEquals("奶茶", student2.getName()); Assert.assertNotEquals(student3, student2); sqlSession1.close(); }
結果如下:
2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 242282810. 2018-09-29 23:14:26,889 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a] 2018-09-29 23:14:26,897 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==>Preparing: select student_id, name, phone, email, sex, locked, gmt_created, gmt_modified from student where student_id=? 2018-09-29 23:14:26,999 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - ==> Parameters: 2(Integer) 2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==Columns: student_id, name, phone, email, sex, locked, gmt_created, gmt_modified 2018-09-29 23:14:27,085 [main] TRACE [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==Row: 2, 小麗, 13821378271, [email protected], 0, 0, 2018-09-04 18:27:42.0, 2018-09-04 18:27:42.0 2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper.selectByPrimaryKey] - <==Total: 1 2018-09-29 23:14:27,093 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.0 2018-09-29 23:14:27,108 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a] 2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@e70f13a] 2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 242282810 to pool. 2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.3333333333333333 2018-09-29 23:14:27,124 [main] DEBUG [com.homejim.mybatis.mapper.StudentMapper] - Cache Hit Ratio [com.homejim.mybatis.mapper.StudentMapper]: 0.5
以上結果, 分幾個過程解釋:
第一階段:
-
在第一個
SqlSession
中, 查詢出student
物件, 此時傳送了 SQL 語句; -
student
更改了name
屬性; -
SqlSession
再次查詢出student1
物件, 此時不傳送 SQL 語句, 日誌中列印了 「Cache Hit Ratio 」, 代表二級快取使用了, 但是沒有命中。 因為一級快取先作用了。 - 由於是一級快取, 因此, 此時兩個物件是相同的。
-
呼叫了
sqlSession.close()
, 此時將資料序列化並保持到二級快取中。
第二階段:
-
新建立一個
sqlSession.close()
物件; -
查詢出
student2
物件,直接從二級快取中拿了資料, 因此沒有傳送 SQL 語句, 此時查了 3 個物件,但只有一個命中, 因此 命中率 1/3=0.333333; -
查詢出
student3
物件,直接從二級快取中拿了資料, 因此沒有傳送 SQL 語句, 此時查了 4 個物件,但只有一個命中, 因此 命中率 2/4=0.5; -
由於
readOnly=“true”
, 因此student2
和student3
都是反序列化得到的, 為不同的例項。
2.3 配置詳解
檢視 dtd 檔案, 可以看到如下約束:
<!ELEMENT cache (property*)> <!ATTLIST cache type CDATA #IMPLIED eviction CDATA #IMPLIED flushInterval CDATA #IMPLIED size CDATA #IMPLIED readOnly CDATA #IMPLIED blocking CDATA #IMPLIED >
從中可以看出:
-
cache
中可以出現任意多個property
子元素; -
cache
有一些可選的屬性type
,eviction
,flushInterval
,size
,readOnly
,blocking
.
2.3.1 type
type
用於指定快取的實現型別, 預設是PERPETUAL
, 對應的是 mybatis 本身的快取實現類org.apache.ibatis.cache.impl.PerpetualCache
。
後續如果我們要實現自己的快取或者使用第三方的快取, 都需要更改此處。
2.3.2 eviction
eviction
對應的是回收策略, 預設為LRU
。
-
LRU:最近最少使用, 移除最長時間不被使用的物件。
-
FIFO:先進先出, 按物件進入快取的順序來移除物件。
-
SOFT:軟引用, 移除基於垃圾回收器狀態和軟引用規則的物件。
-
WEAK:弱引用, 移除基於垃圾回收器狀態和弱引用規則的物件。
2.3.3 flushInterval
flushInterval
對應重新整理間隔, 單位毫秒, 預設值不設定, 即沒有重新整理間隔, 快取僅僅在重新整理語句時重新整理。
如果設定了之後, 到了對應時間會過期, 再次查詢需要從資料庫中取資料。
2.3.4 size
size
對應為引用的數量,即最多的快取物件資料, 預設為1024
。
2.3.5 readOnly
readOnly
為只讀屬性, 預設為false
-
false:可讀寫, 在建立物件時, 會通過反序列化得到快取物件的拷貝。 因此在速度上會相對慢一點, 但重在安全。
-
true: 只讀, 只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此效能很好, 但如果修改了物件, 有可能會導致程式出問題。
2.3.6 blocking
blocking
為阻塞, 預設值為false
。 當指定為true
時將採用BlockingCache
進行封裝。
使用BlockingCache
會在查詢快取時鎖住對應的Key
,如果快取命中了則會釋放對應的鎖,否則會在查詢資料庫以後再釋放鎖,這樣可以阻止併發情況下多個執行緒同時查詢資料。
2.4 注意事項
-
由於在更新時會重新整理快取, 因此需要注意使用場合:查詢頻率很高, 更新頻率很低時使用, 即經常使用select , 相對較少使用delete ,insert ,update 。
-
快取是以namespace 為單位的,不同namespace 下的操作互不影響。但重新整理快取是重新整理整個namespace 的快取, 也就是你update 了一個, 則整個快取都重新整理了。
-
最好在 「只有單表操作 」 的表的namespace 使用快取, 而且對該表的操作都在這個namespace 中。 否則可能會出現資料不一致的情況。