缓存的作用:提高系统的运行速度,提升查询效率。
mybatis系统中默认定义了两级缓存:一级缓存和二级缓存。
1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。
2、二级缓(也称为全局缓存)存需要手动开启和配置,它是基于namespace级别的缓存。
3、为了提高扩展性。mybatis定义了缓存接口Cache,我们可以通过实现Cache接口来自定义二级缓存。
一级缓存:
也称为SqlSession级别的缓存,一级缓存是一直开启的,也没有办法关闭它。一级缓存其实质就是SqlSession级别的一个Map,把所有当前查询出来的数据都放在这个Map当中。然后下次查询的时候,如果还是当前这个SqlSession会话,那么会首先看这个map中有没有需要的数据,如果有则直接拿出来,没有才向数据库中重新查询,这样就能减轻数据库的使用负担了。
对于SqlSession级别的缓存再进行一下解释,一个SqlSession对象有它自己的一级缓存,新的SqlSession对象也拥有它自己的一级缓存,这两个一级缓存之间是不能共用的。所以当前一级缓存中的东西能不能被使用到,也是一个问题。也因此,一级缓存的作用范围就比较小了。
比如用查询举例,假设查询一个部门信息,第一个SqlSession会话查出了部门,那么就会把这个部门信息放在第一个SqlSession会话的一级缓存中。一旦这个SqlSession会话关闭,第二个SqlSession会话再进来想查询这个部门的时候,就得再去数据库重新查询了。而这些部门信息,一旦保存以后,基本是不会去变动的。所以主观上是希望第一次查询的部门信息,在第二次查询的时候可以知道之前有没有查询过,然后把这个信息也放在缓存里,显然这个一级缓存是完成不了的。这边就会用到二级缓存。
一级缓存的作用:
与数据库同一次会话期间查询到的数据会放在本地缓存中;以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库。
一级缓存失效情况:没有使用到当前一级缓存的情况,效果就是,还需要再向数据库发出查询
情况1、SqlSession不同。
情况2、SqlSession相同,但查询条件不同。
情况3、SqlSession相同,但两次查询期间执行了增删改操作。
情况4、SqlSession相同,但手动清除了一级缓存。
二级缓存:
也称为全局缓存,基于namespace级别的缓存。这个namespace就是xml文件中的名称空间,一个namespace对应一个二级缓存。
二级缓存的工作机制:
1、首先用一个SqlSession会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
2、如果会话关闭,一级缓存中的数据会被保存到二级缓存中。新的SqlSession会话查询信息的话,就可以参照二级缓存中的内容;
3、不同namespace查出的数据会放在自己对应的缓存中,在Mybatis内部就是对应的Map中;
举例来说,如果SqlSession中既有EmployeeMapper查出来的Employee对象,又有DepartmentMapper查询出的Department对象。虽然二级缓存是面向全局的,但是这两个对象会放在不同的Map中,因为二级缓存是基于namespace的,这两个对象的namespace又不一样,所以也不会放在相同的Map中了。
二级缓存的使用步骤:
1、开启二级缓存的全局配置;(显式的指定每个需要更改的配置,即使他是默认的,以防版本更新带来的问题)
<setting name="cacheEnabled" value="true" />
2、去mapper.xml中配置使用二级缓存;
解释一下cache标签中各个属性:
eviction:缓存的回收策略;比如说缓存中内容溢出的时候,把哪些内容给删除掉,包括以下这些类型的对象。默认是LRU。
LRU—— 最近最少使用的;移除最长时间不被使用的对象。
FIFO—— 先进先出;按对象进入缓存的顺序来移除他们。
SOFT—— 软引用;移除基于垃圾回收器状态和软引用规则的对象。
WEAK—— 弱引用;更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval:缓存刷新间隔;即缓存多长时间清空一次,默认是不清空,需要的话,可以设置一个毫秒值。
readOnly:缓存是否只读;默认false。
true:只读;mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。
mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,但速度快。
false:非只读;mybatis觉得获取的数据可能会被修改。
mybatis就会利用序列化和反序列化的技术来克隆一份新的数据给用户。安全,但速度慢。
size:缓存存放多少个元素。
type:自定义缓存的时候,指定缓存全类名。但现在用的是默认的,就可以不用写了。
如果选择自定义,那么实现Cache接口就可以了,然后把Cache接口的全类名设为type的值就可以了。
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024" ></cache>
3、POJO需要实现序列化接口;
public class Employee implements Serializable{
private static final long serialVersionUID = 1L;
private Integer id;
private String lastName;
private String email;
private String gender;
private Department dept;
//省略setter、getter方法
}
public class Department implements Serializable{
private static final long serialVersionUID = 1L;
private Integer id;
private String departmentName;
private List<Employee> emps;
//省略setter、getter方法
}
二级缓存使用时的注意事项:
二级缓存的效果是让数据从二级缓存中获取的,但是查出的数据一般都会被默认先放在一级缓存中,然后只有当当前会话关闭或者提交之后,一级缓存中的数据才会转移到二级缓存中。
在哪个mapper.xml文件下写了cache标签,那么哪个mapper.xml文件下就有了二级缓存。没写的话,则没有二级缓存了。
一级缓存体验
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
}finally{
openSession.close();
}
}
现在再添加一次查询
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
Employee emp02 = mapper.getEmpById(1);
System.out.println(emp02);
}finally{
openSession.close();
}
}
发现,虽然查询出来的是两个结果,但是sql语句只调用了一次。这就说明了,第二次查询并没有向数据库中查找,因为第二次要用的数据跟第一次查询出来的结果一样,就直接从缓存中得到了。
用以下语句进行证明
System.out.println(emp01==emp02);
结果证明,这两个对象是相等的
一级缓存失效的情况1:SqlSession不同。
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
SqlSession openSession2 = sqlSessionFactory.openSession();
EmployeeMapper mapper2 = openSession2.getMapper(EmployeeMapper.class);
Employee emp02 = mapper2.getEmpById(1);
System.out.println(emp02);
openSession2.close();
System.out.println(emp01==emp02);
}finally{
openSession.close();
}
}
在两次查询中,都向数据库进行访问了,把查询到的结果放到不同的SqlSession对象中,故而值相同但这两个对象并不相等。
一级缓存失效的情况2:SqlSession相同,但查询条件不同。
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
Employee emp02 = mapper2.getEmpById(3);
System.out.println(emp02);
System.out.println(emp01==emp02);
}finally{
openSession.close();
}
}
第二次查询,必然是要重新向数据库进行查询的,查询完1号员工之后,可当前一级缓存中还没有3号员工的数据呀,所以还是有两条sql语句的。
一级缓存失效的情况3:SqlSession相同,但两次查询期间执行了增删改操作。
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
mapper.addEmp(new Employee(null,"testCache","cache","1"));
System.out.println("数据添加成功")
Employee emp02 = mapper2.getEmpById(1);
System.out.println(emp02);
System.out.println(emp01==emp02);
}finally{
openSession.close();
}
}
失效的原因:在两次查询之间增减了增删改操作,可能会对当前数据有影响。就比如说,如果两个查询之间把这个数据删了或者改掉了,那不就是查询结果不一样了吗。就算是mybatis也是如此,执行了增删改之后,两次查询都还是会重新向数据库进行访问的。
一级缓存失效的情况4:SqlSession相同,但手动清除了一级缓存。
@Test
public void testFirstLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
openSession.clearCache();
Employee emp02 = mapper2.getEmpById(1);
System.out.println(emp02);
System.out.println(emp01==emp02);
}finally{
openSession.close();
}
}
清理了缓存之后,当然需要重新向数据库中进行访问啦,缓存里已经没有原先的1号员工的数据了。
二级缓存体验
根据前面的配置,可以直接测试
@Test
public void testSecondLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
SqlSession openSession2 = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
EmployeeMapper mapper2 = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
openSession.close();
Employee emp02 = mapper2.getEmpById(1);
System.out.println(emp02);
openSession2.close();
}finally{
}
}
可以发现,第二次查询是从二级缓存中拿到的数据,并没有发送新的sql语句。因为第一次会话就已经查出了1号员工,然后这和会话虽然关闭了,但是这个1号员工查出的数据就保存在EmployeeMapper对应的缓存中。第二次还是用EmployeeMapper来查询1号员工,故而它就直接引用缓存中的数据就行了。
其中有两条缓存命中率的语句,如下图
一个命中率为0.0表示在缓存取第一次,没有取到想到的值,成功0次;命中率为0.5则表示第二次在缓存中取到了所需值,成功了一次。
二级缓存注意事项的测试,把第一次会话的关闭放在所有查询之后,那么效果则是发送两次sql。
@Test
public void testSecondLevelCache() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
SqlSession openSession2 = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
EmployeeMapper mapper2 = openSession.getMapper(EmployeeMapper.class);
Employee emp01 = mapper.getEmpById(1);
System.out.println(emp01);
Employee emp02 = mapper2.getEmpById(1);
System.out.println(emp02);
openSession.close();
openSession2.close();
}finally{
}
}
所以一定要先提交或关闭会话,才能把数据从一级缓存中提起出放到二级缓存中去。