1.引言
合理地使用缓存可以有效的提高程序的执行效率,当程序需要频繁查询相同的数据时,就应该设置缓存,因为频繁的访问数据库会导致访问速度的降低,所以我们应该避免一些重复的访问操作;在MyBatis中存在着两级缓存,一级缓存默认开启,二级缓存则是需要手动开启;
2.缓存的作用域和生命周期
2.1.缓存的作用域
在MyBatis中,无论是一级缓存还是二级缓存,都是以namespace为作用域的,也就是说,一个作用域对应着一个缓存,彼此之间互不干扰;
2.2.缓存的生命周期
在MyBatis中,一级缓存的生命周期和SqlSession同步,当SqlSession关闭时,一级缓存也就失效了,MyBatis默认开启一级缓存;二级缓存的生命周期则是和SqlSessionFactory同步,默认是关闭的;
3.一级缓存
在MyBatis中,默认开启一级缓存,并且不能关闭,我们可以通过代码来证明一级缓存的存在依据
当程序执行了增删改的操作后,缓存中的数据就会被清空,这是为了保证数据的一致性,当我们对数据库执行了增删改的操作后,其数据已经发生了变化,而此时执行查询操作后,因为缓存控件的数据应该被清空了,所以程序会从数据库将数据查找出来并添加到缓存空间中,这样就能保证我们从缓存中获取的数据是最新的数据了;
3.1.证明一级缓存的存在
//证明Mybatis一级缓存存在的依据
@Test
public void test01() {
Student student1 = dao.selectStudentById(1);
System.out.println(student1);
Student student2 = dao.selectStudentById(1);
System.out.println(student2);
}
<!--console的输出-->
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
Student [id=1, name=张三, age=23, score=93.5]
可以从控制台的输出中看出,我们执行两次相同的查询操作,第一次查询触发了SQL的运行,而第二次则是直接输出,并没有执行SQL语句,证明了MyBatis的一级缓存是默认开启的;
3.2.从一级缓存中读取数据的依据是:按照SQL的id和SQL语句来查询
我们已经证明了一级缓存的存在,但是程序是如何从缓存中获取数据呢,或者说数据在缓存中如何存放的,其实数据在缓存中是存放在一个Map中的,我们每次访问缓存的时候都会通过其key去获取对应的value,在Hibernate中,它的缓存是以对象的id作为其Key,而在MyBatis中是以SQL的id 和SQL语句作为Map的key的;
//从一级缓存中读取数据的依据是:SQL的id 和 SQL语句
//而Hibernate从缓存中读取数据的依据是:对象的id
@Test
public void test02() {
Student student1 = dao.selectStudentById(1);
System.out.println(student1);
//我们定义了一个和selectStudentById()完全一样的方法selectStudentById2()
//当访问相同的数据,结果console显示执行了两次查询
//也就是说mybatis从一级缓存读取数据是以SQL的id(statement)作为其依据的
//因为一个查询可以用来访问多个数据,所以mybatis是以SQL的id和SQL语句作为其查询依据的
Student student2 = dao.selectStudentById2(1);
System.out.println(student2);
}
<!--console输出信息-->
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
3.3.证明一级缓存的生命周期是以SQLSession同步的
@Test
public void test01() {
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
Student student1 = dao.selectStudentById(1);
System.out.println(student1);
//当执行完一次查询操作后就将sqlSession关闭,然后再重新开启sqlSession
//可以从控制台的输出中看到第二次查询也执行了SQL语句
//也就是说,当SQLSession关闭的时候,缓存也就随之清空了
//这也验证了一级缓存和sqlSession的生命周期同步
sql.close();
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
Student student2 = dao.selectStudentById(1);
System.out.println(student2);
}
<!--console输出信息-->
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
3.4.证明执行增删改操作会触发对缓存清空
@Test
public void test01() {
Student student1 = dao.selectStudentById(1);
System.out.println(student1);
//执行了插入操作后我们可以看到控制台的输出中,第二次查询也触发了SQL的执行
//也就是说当程序执行增删改操作后会将缓存中的数据清空
dao.insertStudent(new Student("赵六",26,96.5));
Student student2 = dao.selectStudentById(1);
System.out.println(student2);
}
<!--console输出信息-->
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - ==> Preparing: insert into student(name,age,score) values(?,?,?)
[main] DEBUG - ==> Parameters: 赵六(String), 26(Integer), 96.5(Double)
[main] DEBUG - <== Updates: 1
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
4.二级缓存
<!--二级缓存的使用原则-->
1)多个namespace不要操作同一张表 2)不要在关联关系表上执行增删改操作 3)查询多于修改时使用二级缓存
在实际的项目中我们很少使用内置的二级缓存,这是因为实际项目中的表基本上都有着关联关系关系,很少操作会只涉及到一张表(namespace),所以我们很多时候都不会使用内置的二级缓存;而是会选择使用第三方的二级缓存,例如Ehcache二级缓存;
4.1.开启二级缓存
1)实体类实现序列化
2)在映射文件中开启<cache/>标签
<!--1)Student.class-->
public class Student implements Serializable{...}
<!--2)mapper.xml-->
<cache/>
@Test
public void test01() {
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
Student stu1 = dao.selectStudentById(1);
System.out.println(stu1);
//想要测试二级缓存就必须去除掉一级缓存的影响,因为一级缓存的生命周期和sqlSession同步
//所以在第一次查询后将sqlSession关闭,这样一级缓存就失效了
//在第二次查询之前重新获取sqlSession,这样就能看到二级缓存的效果了
sql.close();
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
Student stu2 = dao.selectStudentById(1);
System.out.println(stu2);
}
<!--console输出信息-->
//Cache Hit Ratio 的值表示的是查询命中率
//第一次查询因为缓存中没有该值,所以命中率为0.0
//第二次查询的时候就查找到该值,所以命中率就为0.5 命中率 = 命中次数 / 查询次数
[main] DEBUG - Cache Hit Ratio [com.mybatis.dao.StudentDao]: 0.0
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - Cache Hit Ratio [com.mybatis.dao.StudentDao]: 0.5
Student [id=1, name=张三, age=23, score=93.5]
4.2.证明增删改操作对二级缓存的影响
@Test
public void test01() {
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
Student stu1 = dao.selectStudentById(1);
System.out.println(stu1);
sql.close();
sql = SqlSessionUtil.getSqlSession();
dao = sql.getMapper(StudentDao.class);
//当执行了第一次查询之后就执行插入操作,此时会清空一级缓存的内容
//然后关闭sqlSession,执行第二次查询
//根据console我们可以看到第二次查询也执行了SQL语句,说明增删改对二级缓存同样起作用
//资料得:执行增删改的操作后二级缓存并没有将其key删除,而是将key所对应的value置为null
//所以我们可以推导出二级缓存起作用的条件:存在该key并且该key所对应的value不为null
dao.insertStudent(new Student("",0,0.0));
Student stu2 = dao.selectStudentById(1);
System.out.println(stu2);
}
<!--console输出信息-->
[main] DEBUG - Cache Hit Ratio [com.mybatis.dao.StudentDao]: 0.0
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - ==> Preparing: insert into student(name,age,score) values(?,?,?)
[main] DEBUG - ==> Parameters: (String), 0(Integer), 0.0(Double)
[main] DEBUG - <== Updates: 1
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
我们也可以通过让 增删改操作 对二级缓存不起作用,只要修改mapper文件中的增删改操作信息即可(不推荐);
<!--mapper.xml 修改了flushCache属性的值为false-->
<insert id="insertStudent" parameterType="Student" flushCache="false">
insert into student(name,age,score) values(#{name},#{age},#{score})
</insert>
<!--console输出信息-->
[main] DEBUG - Cache Hit Ratio [com.mybatis.dao.StudentDao]: 0.0
[main] DEBUG - ==> Preparing: select id,name,age,score from student where id=?
[main] DEBUG - ==> Parameters: 1(Integer)
[main] DEBUG - <== Total: 1
Student [id=1, name=张三, age=23, score=93.5]
[main] DEBUG - ==> Preparing: insert into student(name,age,score) values(?,?,?)
[main] DEBUG - ==> Parameters: (String), 0(Integer), 0.0(Double)
[main] DEBUG - <== Updates: 1
[main] DEBUG - Cache Hit Ratio [com.mybatis.dao.StudentDao]: 0.5
Student [id=1, name=张三, age=23, score=93.5]
4.3.关闭二级缓存
关闭二级缓存有两种方式,分为全局关闭和局部关闭
1 ) 全局关闭 : 全局关闭需要在`mybatis.xml`中关闭,通过设置`cacheEnabled`的值,对整个应用起作用
<!--全局关闭二级缓存-->
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
2)局部关闭,在mapper.xml中关闭,设置flushCache的值,对当前操作起作用
<!--局部关闭二级缓存-->
<select id="selectStudentById" resultType="Student" useCache="false">
select id,name,age,score from student where id=#{id}
</select>
5.EhCache二级缓存:如何开启EhCache二级缓存
1) 导入jar包:ehcache-core-2.6.11.jar、mybatis-ehcache-1.1.0.jar
2) 在mapper中配置ehcache:
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
3) 在classpath中添加ehcache的配置文件:ehcache.xml,该文件在ehcache-core-2.6.11.jar可以解压得到ehcache-failsafe.xml,将其重命名为ehcache.xml;
之前使用内置的二级缓存时,实体类是需要实现序列化,而ehCache二级缓存则不需要实体类实现序列化;