緩存的重要性是不言而喻的。 使用緩存, 我們可以避免頻繁的與數據庫進行交互, 尤其是在查詢越多、緩存命中率越高的情況下, 使用緩存對性能的提高更明顯。
mybatis 也提供了對緩存的支持, 分爲一級緩存和二級緩存。 但是在默認的情況下, 只開啓一級緩存(一級緩存是對同一個 SqlSession 而言的)。
以下的項目是在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 中。 否則可能會出現數據不一致的情況。