mybatis 快取的使用, 看這篇就夠了
快取的重要性是不言而喻的。 使用快取, 我們可以避免頻繁的與資料庫進行互動, 尤其是在查詢越多、快取命中率越高的情況下, 使用快取對效能的提高更明顯。
mybatis 也提供了對快取的支援, 分為一級快取和二級快取。 但是在預設的情況下, 只開啟一級快取(一級快取是對同一個 SqlSession 而言的)。
對以下的程式碼, 你也可以從我的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 [[email protected]]
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 [[email protected]]
2018-09-29 23:14:27,116 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [[email protected]]
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 中。 否則可能會出現資料不一致的情況。