一級緩存
Mybatis對緩存提供支持,但是在沒有配置的默認(rèn)情況下,它只開啟一級緩存,一級緩存只是相對于同一個SqlSession而言。所以在參數(shù)和SQL完全一樣的情況下,我們使用同一個SqlSession對象調(diào)用一個Mapper方法,往往只執(zhí)行一次SQL,因?yàn)槭褂肧elSession第一次查詢后,MyBatis會將其放在緩存中,以后再查詢的時候,如果沒有聲明需要刷新,并且緩存沒有超時的情況下,SqlSession都會取出當(dāng)前緩存的數(shù)據(jù),而不會再次發(fā)送SQL到數(shù)據(jù)庫。

為什么要使用一級緩存,不用多說也知道個大概。但是還有幾個問題我們要注意一下。
1、一級緩存的生命周期有多長?
a、MyBatis在開啟一個數(shù)據(jù)庫會話時,會 創(chuàng)建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象。Executor對象中持有一個新的PerpetualCache對象;當(dāng)會話結(jié)束時,SqlSession對象及其內(nèi)部的Executor對象還有PerpetualCache對象也一并釋放掉。
b、如果SqlSession調(diào)用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用。
c、如果SqlSession調(diào)用了clearCache(),會清空PerpetualCache對象中的數(shù)據(jù),但是該對象仍可使用。
d、SqlSession中執(zhí)行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用
2、怎么判斷某兩次查詢是完全相同的查詢?
mybatis認(rèn)為,對于兩次查詢,如果以下條件都完全一樣,那么就認(rèn)為它們是完全相同的兩次查詢。
2.1 傳入的statementId
2.2 查詢時要求的結(jié)果集中的結(jié)果范圍
2.3. 這次查詢所產(chǎn)生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字符串(boundSql.getSql() )
2.4 傳遞給java.sql.Statement要設(shè)置的參數(shù)值
二級緩存:
MyBatis的二級緩存是Application級別的緩存,它可以提高對數(shù)據(jù)庫查詢的效率,以提高應(yīng)用的性能。
MyBatis的緩存機(jī)制整體設(shè)計(jì)以及二級緩存的工作模式

SqlSessionFactory層面上的二級緩存默認(rèn)是不開啟的,二級緩存的開啟需要進(jìn)行配置,實(shí)現(xiàn)二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的。 也就是要求實(shí)現(xiàn)Serializable接口,配置方法很簡單,只需要在映射XML文件配置就可以開啟緩存了<cache/>,如果我們配置了二級緩存就意味著:
- 映射語句文件中的所有select語句將會被緩存。
- 映射語句文件中的所欲insert、update和delete語句會刷新緩存。
- 緩存會使用默認(rèn)的Least Recently Used(LRU,最近最少使用的)算法來收回。
- 根據(jù)時間表,比如No Flush Interval,(CNFI沒有刷新間隔),緩存不會以任何時間順序來刷新。
- 緩存會存儲列表集合或?qū)ο?無論查詢方法返回什么)的1024個引用
- 緩存會被視為是read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而且可以安全的被調(diào)用者修改,不干擾其他調(diào)用者或線程所做的潛在修改。
實(shí)踐:
一、創(chuàng)建一個POJO Bean并序列化
由于二級緩存的數(shù)據(jù)不一定都是存儲到內(nèi)存中,它的存儲介質(zhì)多種多樣,所以需要給緩存的對象執(zhí)行序列化。(如果存儲在內(nèi)存中的話,實(shí)測不序列化也可以的。)
import java.io.Serializable;
import java.util.List;
public class Student implements Serializable {
private static final long serialVersionUID = 735655488285535299L;
private String id;
private String name;
private int age;
private Gender gender;
private List<Teacher> teachers;
setters&getters()....;
toString();
}
二、在映射文件中開啟二級緩存
cache配置
| 屬性 | 說明 | 默認(rèn)值 | 可選值 |
|---|---|---|---|
| eviction | 回收內(nèi)存策略 | LRU | LRU FIFO SOFT WEAK |
| flushInterval | 刷新間隔 | 沒設(shè)置 | 大于0 (單位:ms) |
| size | 緩存對象的數(shù)量 | 1024 | 大于0 |
| readOnly | 緩存數(shù)據(jù)只能讀取而不能修改 | false | true false |
| type | 可以指定自定義緩存,但是該類必須實(shí)現(xiàn)org.apache.ibatis.cache.Cache接口 | com....class |
自定義緩存
該屬性會調(diào)用setCacheFile方法(setter),將屬性值注入。
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
開啟本mapper的namespace下的二級緩存
eviction:代表的是緩存回收策略
- LRU,最近最少使用的,一處最長時間不用的對象
- FIFO,先進(jìn)先出,按對象進(jìn)入緩存的順序來移除他們
- SOFT,軟引用,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象
- WEAK,弱引用,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。
例子采用LRU,移除最長時間不用的對形象 flushInterval:刷新間隔時間,單位為毫秒,這里配置的是100秒刷新,如果你不配置它,那么當(dāng)SQL被執(zhí)行的時候才會去刷新緩存。
size:引用數(shù)目,一個正整數(shù),代表緩存最多可以存儲多少個對象,不宜設(shè)置過大。設(shè)置過大會導(dǎo)致內(nèi)存溢出,這里配置的是1024個對象。
readOnly:只讀,意味著緩存數(shù)據(jù)只能讀取而不能修改,這樣設(shè)置的好處是我們可以快速讀取緩存,缺點(diǎn)是我們沒有辦法修改緩存,他的默認(rèn)值是false,不允許我們修改。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo.mybatis.dao.StudentMapper">
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
<resultMap id="studentMap" type="Student">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="gender" column="gender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
</resultMap>
<resultMap id="collectionMap" type="Student" extends="studentMap">
<collection property="teachers" ofType="Teacher">
<id property="id" column="teach_id" />
<result property="name" column="tname"/>
<result property="gender" column="tgender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result property="subject" column="tsubject" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
<result property="degree" column="tdegree" javaType="string" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<select id="selectStudents" resultMap="collectionMap">
SELECT
s.id, s.name, s.gender, t.id teach_id, t.name tname, t.gender tgender
FROM
student s
LEFT JOIN
stu_teach_rel str
ON
s.id = str.stu_id
LEFT JOIN
teacher t
ON
t.id = str.teach_id
</select>
<!--可以通過設(shè)置useCache來規(guī)定這個sql是否開啟緩存,ture是開啟,false是關(guān)閉-->
<select id="selectAllStudents" resultMap="studentMap" useCache="true">
SELECT id, name, age FROM student
</select>
<!--刷新二級緩存
<select id="selectAllStudents" resultMap="studentMap" flushCache="true">
SELECT id, name, age FROM student
</select> -->
</mapper>
三、在 mybatis-config.xml中開啟二級緩存
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--這個配置使全局的映射器(二級緩存)啟用或禁用緩存-->
<setting name="cacheEnabled" value="true" />
.....
</settings>
....
</configuration>
四、測試
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.util.List;
public class TestStudent extends BaseTest {
public static void selectAllStudent() {
SqlSessionFactory sqlSessionFactory = getSession();
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);
List<Student> list = mapper.selectAllStudents();
System.out.println(list);
System.out.println("第二次執(zhí)行");
List<Student> list2 = mapper.selectAllStudents();
System.out.println(list2);
session.commit();
System.out.println("二級緩存觀測點(diǎn)");
SqlSession session2 = sqlSessionFactory.openSession();
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
List<Student> list3 = mapper2.selectAllStudents();
System.out.println(list3);
System.out.println("第二次執(zhí)行");
List<Student> list4 = mapper2.selectAllStudents();
System.out.println(list4);
session2.commit();
}
public static void main(String[] args) {
selectAllStudent();
}
}
結(jié)果:
我們可以從結(jié)果看到,sql只執(zhí)行了一次,證明我們的二級緩存生效了。
探究二級緩存
我們繼續(xù)以 MyBatis 一級緩存文章中的例子為基礎(chǔ),搭建一個滿足二級緩存的例子,來對二級緩存進(jìn)行探究,例子如下(對 一級緩存的例子部分源碼進(jìn)行修改):
Dept.java
存放在共享緩存中數(shù)據(jù)進(jìn)行序列化操作和反序列化操作,實(shí)體類必須實(shí)現(xiàn)序列化接口。
public class Dept implements Serializable {
private Integer deptNo;
private String dname;
private String loc;
public Dept() {}
public Dept(Integer deptNo, String dname, String loc) {
this.deptNo = deptNo;
this.dname = dname;
this.loc = loc;
}
get and set...
}
對應(yīng)的二級緩存測試類如下:
public class MyBatisSecondCacheTest {
private SqlSession sqlSession;
SqlSessionFactory factory;
@Before
public void start() throws IOException {
InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builderObj = new SqlSessionFactoryBuilder();
factory = builderObj.build(is);
sqlSession = factory.openSession();
}
@After
public void destory(){
if(sqlSession!=null){
sqlSession.close();
}
}
@Test
public void testSecondCache(){
//會話過程中第一次發(fā)送請求,從數(shù)據(jù)庫中得到結(jié)果
//得到結(jié)果之后,mybatis自動將這個查詢結(jié)果放入到當(dāng)前用戶的一級緩存
DeptDao dao = sqlSession.getMapper(DeptDao.class);
Dept dept = dao.findByDeptNo(1);
System.out.println("第一次查詢得到部門對象 = "+dept);
//觸發(fā)MyBatis框架從當(dāng)前一級緩存中將Dept對象保存到二級緩存
sqlSession.commit();
// 改成 sqlSession.close(); 效果相同
SqlSession session2 = factory.openSession();
DeptDao dao2 = session2.getMapper(DeptDao.class);
Dept dept2 = dao2.findByDeptNo(1);
System.out.println("第二次查詢得到部門對象 = "+dept2);
}
}
測試二級緩存效果,提交事務(wù),
sqlSession查詢完數(shù)據(jù)后,sqlSession2相同的查詢是否會從緩存中獲取數(shù)據(jù)。
測試結(jié)果如下:
通過結(jié)果可以得知,首次執(zhí)行的SQL語句是從數(shù)據(jù)庫中查詢得到的結(jié)果,然后第一個 SqlSession 執(zhí)行提交,第二個 SqlSession 執(zhí)行相同的查詢后是從緩存中查取的。
用一下這幅圖能夠比較直觀的反映兩次 SqlSession 的緩存命中

二級緩存失效的條件
與一級緩存一樣,二級緩存也會存在失效的條件的,下面我們就來探究一下哪些情況會造成二級緩存失效
-
第一次SqlSession 未提交
SqlSession 在未提交的時候,SQL 語句產(chǎn)生的查詢結(jié)果還沒有放入二級緩存中,這個時候 SqlSession2 在查詢的時候是感受不到二級緩存的存在的,修改對應(yīng)的測試類,結(jié)果如下:
@Test
public void testSqlSessionUnCommit(){
//會話過程中第一次發(fā)送請求,從數(shù)據(jù)庫中得到結(jié)果
//得到結(jié)果之后,mybatis自動將這個查詢結(jié)果放入到當(dāng)前用戶的一級緩存
DeptDao dao = sqlSession.getMapper(DeptDao.class);
Dept dept = dao.findByDeptNo(1);
System.out.println("第一次查詢得到部門對象 = "+dept);
//觸發(fā)MyBatis框架從當(dāng)前一級緩存中將Dept對象保存到二級緩存
SqlSession session2 = factory.openSession();
DeptDao dao2 = session2.getMapper(DeptDao.class);
Dept dept2 = dao2.findByDeptNo(1);
System.out.println("第二次查詢得到部門對象 = "+dept2);
}
產(chǎn)生的輸出結(jié)果:

-
更新對二級緩存影響
與一級緩存一樣,更新操作很可能對二級緩存造成影響,下面用三個 SqlSession來進(jìn)行模擬,第一個 SqlSession 只是單純的提交,第二個 SqlSession 用于檢驗(yàn)二級緩存所產(chǎn)生的影響,第三個 SqlSession 用于執(zhí)行更新操作,測試如下:
@Test
public void testSqlSessionUpdate(){
SqlSession sqlSession = factory.openSession();
SqlSession sqlSession2 = factory.openSession();
SqlSession sqlSession3 = factory.openSession();
// 第一個 SqlSession 執(zhí)行更新操作
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
Dept dept = deptDao.findByDeptNo(1);
System.out.println("dept = " + dept);
sqlSession.commit();
// 判斷第二個 SqlSession 是否從緩存中讀取
DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
Dept dept2 = deptDao2.findByDeptNo(1);
System.out.println("dept2 = " + dept2);
// 第三個 SqlSession 執(zhí)行更新操作
DeptDao deptDao3 = sqlSession3.getMapper(DeptDao.class);
deptDao3.updateDept(new Dept(1,"ali","hz"));
sqlSession3.commit();
// 判斷第二個 SqlSession 是否從緩存中讀取
dept2 = deptDao2.findByDeptNo(1);
System.out.println("dept2 = " + dept2);
}
對應(yīng)的輸出結(jié)果如下

探究多表操作對二級緩存的影響
現(xiàn)有這樣一個場景,有兩個表,部門表dept(deptNo,dname,loc)和 部門數(shù)量表deptNum(id,name,num),其中部門表的名稱和部門數(shù)量表的名稱相同,通過名稱能夠聯(lián)查兩個表可以知道其坐標(biāo)(loc)和數(shù)量(num),現(xiàn)在我要對部門數(shù)量表的 num 進(jìn)行更新,然后我再次關(guān)聯(lián)dept 和 deptNum 進(jìn)行查詢,你認(rèn)為這個 SQL 語句能夠查詢到的 num 的數(shù)量是多少?來看一下代碼探究一下
DeptNum.java
public class DeptNum {
private int id;
private String name;
private int num;
get and set...
}
DeptVo.java
public class DeptVo {
private Integer deptNo;
private String dname;
private String loc;
private Integer num;
public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
this.deptNo = deptNo;
this.dname = dname;
this.loc = loc;
this.num = num;
}
public DeptVo(String dname, Integer num) {
this.dname = dname;
this.num = num;
}
}
DeptDao.java
public interface DeptDao {
...
DeptVo selectByDeptVo(String name);
DeptVo selectByDeptVoName(String name);
int updateDeptVoNum(DeptVo deptVo);
}
DeptDao.xml
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
select d.deptno,d.dname,d.loc,dn.num
from dept d,deptNum dn
where dn.name = d.dname
and d.dname = #{name}
</select>
<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
select * from deptNum where name = #{name}
</select>
<update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo">
update deptNum set num = #{num} where name = #{dname}
</update>
DeptNum 數(shù)據(jù)庫初始值:

測試類對應(yīng)如下:
/**
* 探究多表操作對二級緩存的影響
*/
@Test
public void testOtherMapper(){
// 第一個mapper 先執(zhí)行聯(lián)查操作
SqlSession sqlSession = factory.openSession();
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
DeptVo deptVo = deptDao.selectByDeptVo("ali");
System.out.println("deptVo = " + deptVo);
// 第二個mapper 執(zhí)行更新操作 并提交
SqlSession sqlSession2 = factory.openSession();
DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
sqlSession2.commit();
sqlSession2.close();
// 第一個mapper 再次進(jìn)行查詢,觀察查詢結(jié)果
deptVo = deptDao.selectByDeptVo("ali");
System.out.println("deptVo = " + deptVo);
}
測試結(jié)果如下:

在對DeptNum 表執(zhí)行了一次更新后,再次進(jìn)行聯(lián)查,發(fā)現(xiàn)數(shù)據(jù)庫中查詢出的還是 num 為 1050 的值,也就是說,實(shí)際上 1050 -> 1000 ,最后一次聯(lián)查實(shí)際上查詢的是第一次查詢結(jié)果的緩存,而不是從數(shù)據(jù)庫中查詢得到的值,這樣就讀到了臟數(shù)據(jù)。
解決辦法
如果是兩個mapper命名空間的話,可以使用
<cache-ref>來把一個命名空間指向另外一個命名空間,從而消除上述的影響,再次執(zhí)行,就可以查詢到正確的數(shù)據(jù)。
二級緩存整體管理結(jié)構(gòu):
MapperA.xml
<mapper namespace="com.jabnih.demo.mapper.MapperA">
<cache />
</mapper>
MapperB.xml
<mapper namespace="com.jabnih.demo.mapper.MapperB">
<cache-ref namespace="com.jabnih.demo.mapper.MapperA"/>
</mapper>
MapperC.xml
<mapper namespace="com.jabnih.demo.mapper.MapperC">
<cache />
</mapper>
如下:

二級緩存源碼解析
源碼模塊主要分為兩個部分:二級緩存的創(chuàng)建和二級緩存的使用,首先先對二級緩存的創(chuàng)建進(jìn)行分析:
二級緩存的創(chuàng)建
二級緩存的創(chuàng)建是使用 Resource 讀取 XML 配置文件開始的
InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
factory = builder.build(is);
讀取配置文件后,需要對XML創(chuàng)建 Configuration并初始化
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
調(diào)用 parser.parse() 解析根目錄 /configuration 下面的標(biāo)簽,依次進(jìn)行解析
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
其中有一個二級緩存的解析就是
mapperElement(root.evalNode("mappers"));
然后進(jìn)去 mapperElement 方法中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
繼續(xù)跟 mapperParser.parse() 方法
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
這其中有一個 configurationElement 方法,它是對二級緩存進(jìn)行創(chuàng)建,如下
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
有兩個二級緩存的關(guān)鍵點(diǎn)
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
也就是說,mybatis 首先進(jìn)行解析的是 cache-ref 標(biāo)簽,其次進(jìn)行解析的是 cache 標(biāo)簽。
根據(jù)上面我們的 — 多表操作對二級緩存的影響 一節(jié)中提到的解決辦法,采用 cache-ref 來進(jìn)行命名空間的依賴能夠避免二級緩存,但是總不能每次寫一個 XML 配置都會采用這種方式吧,最有效的方式還是避免多表操作使用二級緩存
然后我們再來看一下cacheElement(context.evalNode("cache")) 這個方法
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
認(rèn)真看一下其中的屬性的解析,是不是感覺很熟悉?這不就是對 cache 標(biāo)簽屬性的解析嗎??。?!
上述最后一句代碼
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
這段代碼使用了構(gòu)建器模式,一步一步構(gòu)建Cache 標(biāo)簽的所有屬性,最終把 cache 返回。
二級緩存的使用
在 mybatis 中,使用 Cache 的地方在 CachingExecutor中,來看一下 CachingExecutor 中緩存做了什么工作,我們以查詢?yōu)槔?/p>
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 得到緩存
Cache cache = ms.getCache();
if (cache != null) {
// 如果需要的話刷新緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托模式,交給SimpleExecutor等實(shí)現(xiàn)類去實(shí)現(xiàn)方法。
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
其中,先從 MapperStatement 取出緩存。只有通過<cache/>,<cache-ref/>或@CacheNamespace,@CacheNamespaceRef標(biāo)記使用緩存的Mapper.xml或Mapper接口(同一個namespace,不能同時使用)才會有二級緩存。
如果緩存不為空,說明是存在緩存。如果cache存在,那么會根據(jù)sql配置(<insert>,<select>,<update>,<delete>的flushCache屬性來確定是否清空緩存。
flushCacheIfRequired(ms);
然后根據(jù)xml配置的屬性useCache來判斷是否使用緩存(resultHandler一般使用的默認(rèn)值,很少會null)。
if (ms.isUseCache() && resultHandler == null)
確保方法沒有Out類型的參數(shù),mybatis不支持存儲過程的緩存,所以如果是存儲過程,這里就會報(bào)錯。
private void ensureNoOutParams(MappedStatement ms, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
然后根據(jù)在 TransactionalCacheManager 中根據(jù) key 取出緩存,如果沒有緩存,就會執(zhí)行查詢,并且將查詢結(jié)果放到緩存中并返回取出結(jié)果,否則就執(zhí)行真正的查詢方法。
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
是否應(yīng)該使用二級緩存?
那么究竟應(yīng)該不應(yīng)該使用二級緩存呢?先來看一下二級緩存的注意事項(xiàng):
- 緩存是以
namespace為單位的,不同namespace下的操作互不影響。 - insert,update,delete操作會清空所在
namespace下的全部緩存。 - 通常使用MyBatis Generator生成的代碼中,都是各個表獨(dú)立的,每個表都有自己的
namespace。 - 多表操作一定不要使用二級緩存,因?yàn)槎啾聿僮鬟M(jìn)行更新操作,一定會產(chǎn)生臟數(shù)據(jù)。
如果你遵守二級緩存的注意事項(xiàng),那么你就可以使用二級緩存。
但是,如果不能使用多表操作,二級緩存不就可以用一級緩存來替換掉嗎?而且二級緩存是表級緩存,開銷大,沒有一級緩存直接使用 HashMap 來存儲的效率更高,所以二級緩存并不推薦使用。