缓存:
查询需要连接数据库,非常的耗费资源,将一次查询的结果,暂存在一个可以直接取到的地方,我们将其称之为缓存,当我们需要再次查询相同的数据时,直接走缓存这个过程,就不用走数据库了
缓存的概念:
存在内存中的临时数据,通过将用户经常查询的数据放在缓存[内存]中,用户去查询数据就不用从磁盘上(关系数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题
使用缓存的原因:
减少和数据库的交互次数,减少系统开销,提高系统效率
适合缓存的数据:
经常查询并且不太变化的数据是比较适合缓存的
Mybatis缓存:
mybatis包含一个非常强大的查询缓存特性,他可以非常方便地定制和配置缓存,缓存可以极大的提升查询效率,mybatis系统中默认定义了两级缓存:一级缓存和二级缓存,默认情况下,只有一级缓存开启{SqlSession级别的缓存,也称为本地缓存}
二级缓存需要手动开启和配置,它是基于namespace级别的缓存,为了提高扩展性,mybatis定义了缓存接口Cache,我们可以通过实现Cache接口来定义二级缓存
一级缓存:
一级缓存也叫本地缓存:SqlSession
与数据库同一次会话期间查询到的数据会放在本地缓存中,在以后的查询工作中,如果需要获取相同的数据,直接从缓存中拿,不用再去查询数据库
举例:
关于测试本案例的其他四个文件代码,在这篇文章有,这里就不过多赘述了,不过需要注意的是mybatis-config.xml文件中绑定的接口名不要忘记更换
一级缓存的使用:
在接口中编写方法:
package dao;
import org.apache.ibatis.annotations.Param;
import pojo.Student;
public interface StudentMapper {
//根据id查询用户
Student queryStudentById(@Param("id") int id);
}
StudentMapper.xml文件中编写SQL语句:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.StudentMapper">
<select id="queryStudentById" resultType="Student">
select * from student where id=#{id}
</select>
</mapper>
实体类:
注:实体类所对应的数据表应提前创建好,并插入数据,注意字段一致
package pojo;
import lombok.Data;
@Data
public class Student {
private int id;
private String name;
private String password;
}
测试类:
使用同一个SqlSession查询同一组数组两次:
package dao.user;
import dao.StudentMapper;
import pojo.Student;
import utils.mybatis_utils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyTest{
@Test
public void getUserByLimit(){
//打开会话
SqlSession sqlSession= mybatis_utils.getSqlSession();
StudentMapper studentMapper=sqlSession.getMapper(StudentMapper.class);
//第一次查询id为2的数据
Student student1=studentMapper.queryStudentById(2);
System.out.println(student1);
System.out.println("============================");
//第二次查询id为2的数据
Student student2=studentMapper.queryStudentById(2);
System.out.println(student2);
//判断student1和student2是否为同一个对象
System.out.println(student1==student2);
//关闭会话
sqlSession.close();
}
}
输出如下:
通过输出的日志文件,我们可以清楚的看到当查询同一个数据两次
时,预编译过程只会加载一次,并且通过比较他两是否相等的输出结果,也可以看出,他两确实是连地址都是相同的,那么也足以说明,与数据库同一次会话期间查询到的数据会放在本地缓存中,在以后的查询工作中,如果需要获取相同的数据,直接从缓存中拿,不用再去查询数据库,也就是不需要进行预编译这个过程
一级缓存失效的情况:
1:查询不同数据
举例:
修改查询数据为id等于1和id等于2:
Student student1=studentMapper.queryStudentById(2);
System.out.println(student1);
System.out.println("============================");
Student student2=studentMapper.queryStudentById(1);
System.out.println(student2);
System.out.println("student1和student2是否相等?"+(student1==student2));
输出如下:
当两次查询的数据不同时,我们会发现,其预编译过程进行了两次,并且通过比较发现他两并不相等
2:增删改操作,可能会改变原来的数据,所以必定会刷新缓存
举例{这里我们以更新操作为例}:
此次我们进行的操作步骤共有3个:
//1:查询id为1的数据
Student student1=studentMapper.queryStudentById(1);
System.out.println(student1);
//2:更新id为2的数据
studentMapper.updateStudent(new Student(2,"ABC","11000"));
System.out.println("============================");
//3:再次查询id为1的数据
Student student2=studentMapper.queryStudentById(1);
System.out.println(student2);
System.out.println("student1和student2是否相等?"+(student1==student2));
输出如下所示:
通过输出结果,我们会发现,在第一次查询完id为1的数据后,我们对id为2的数据进行了更新操作,虽然id为1的数据并没有做任何的改变,但它此时同样预编译了2次,即缓存失效
3:查询不同的.xml文件
不同的.xml文件绑定不同的接口,sqlSession.getMapper获取到的值都不相同了,缓存当然失效了
4:手动清理缓存
方法如下:
在第一次查询结束时,调用clearCache方法手动清除缓存
//查询id为1的数据
Student student1=studentMapper.queryStudentById(1);
System.out.println(student1);
//手动清除缓存
sqlSession.clearCache();
System.out.println("============================");
//再次查询id为1的数据
Student student2=studentMapper.queryStudentById(1);
System.out.println(student2);
System.out.println("student1和student2是否相等?"+(student1==student2));
输出如下:
即使我们两次查询的内容都为id等于1,但预编译过程也进行了两次,说明此时一级缓存失效
一级缓存默认是开启的,只在一次SqlSession中有效
,也就是从开始连接到关闭连接的这个区间段
如下所示:
二级缓存:
二级缓存也叫全局缓存,由于一级缓存作用域太低了,所以诞生了二级缓存
,它是基于namespace级别的缓存,一个名称空间,对应一个二级缓存
工作机制:
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中,如果当前会话关闭了,这个会话对应的一级缓存就没有了,但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中,新的会话查询信息,就可以从二级缓存中获取内容,不同的mapper查出的数据会放在自己对应的缓存(map)中
默认情况下,只启用了本地的会话缓存[一级缓存]
,它仅仅对一个会话中的数据进行缓存,如果要启用全局的二级缓存,只需要在SQL 映射文件中添加:
方法1:
<cache/>
这个简单语句的效果如下:
1: 映射语句文件中的所有 select 语句的结果将会被缓存
2: 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存
3: 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存
4: 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
5: 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用
6:缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
注: 缓存只作用于 cache 标签所在的映射文件中的语句,如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存,你需要在mapper接口上使用 @CacheNamespaceRef 注解指定缓存作用域
方法2:
<cache eviction="FIFO"
flushInterval="60000"
size="512":
readOnly="true"/>
上述两种方法的主要区别在于,使用方式1,如果实体类未实现序列化接口,那么会报错,而第二种方法因为设置了readOnly参数的值,因此不会出现这种情况
二级缓存的使用步骤:
1:开启全局缓存:
官方文档中虽然指出二级缓存是默认开启的
,但我们最好在配置文件的settings标签中显式的写出来,原因是为了增强可读性,当别人阅读你的代码时,可以很快的知道二级缓存确实开启了
在核心配置文件的settings标签中增加:如下代码
<setting name="cacheEnabled" value="true"/>
2:在要使用二级缓存的Mapper.xml文件中开启
方法1:
<cache/>
如果你是用方法1,报错如下:
原因:<cache/>
中的readOnly默认为false,而可读写的缓存会通过序列化返回缓存对象的拷贝,此时需要实体类(这里是User)实现Serializable接口或者配置readOnly=true
解决方法:对实体类进行序列化,如下所示
方法1:在实体类中实现序列化接口
implements Serializable
输出如下所示:
注:序列化是深拷贝,所以反序列化后的对象和原对象不是同一个对象,故哈希值不同,因此输出false
方法2:在cache标签中,将readOnly参数的值设置为true
<cache readOnly="true"/>
输出如下:
方法2:自定义参数
注:使用方法2未报上面那种错误的原因为:在设置参数时,我们将readOnly的值设置为true
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
3:测试类中测试
package dao.user;
import dao.StudentMapper;
import pojo.Student;
import utils.mybatis_utils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class MyTest{
@Test
public void getUserByLimit(){
//同时开启两个SqlSession去查询同一个数据
SqlSession sqlSession1= mybatis_utils.getSqlSession();
SqlSession sqlSession2= mybatis_utils.getSqlSession();
StudentMapper studentMapper1=sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2=sqlSession2.getMapper(StudentMapper.class);
//使用会话1查询id为1的数据
Student student1=studentMapper1.queryStudentById(1);
System.out.println(student1);
//使用会话2查询id为1的数据
Student student2=studentMapper2.queryStudentById(1);
System.out.println(student2);
System.out.println(student1==student2);
//同时关闭两个会话
sqlSession1.close();
sqlSession2.close();
}
}
输出如下:
根据二级缓存的工作机制:
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中
,如果当前会话关闭了,这个会话对应的一级缓存就没有了,但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中,新的会话查询信息,就可以从二级缓存中获取内容
而我们上述写法会话1和会话2同时打开同时关闭的做法显然不符合二级缓存工作机制。
因此,修改如下所示,我们先关闭SqlSession1后再查询数据,最后关闭SqlSession2:
输出如下:
我们会发现当SqlSession1会话关闭后,当再次查询与会话1查询相同的数据时,并没有进行预编译的过程,原因是:一级缓存中的数据被保存到二级缓存中,新的会话查询信息就可以从二级缓存中获取内容,因此只进行一次预编译,且通过比较发现他两是同一个对象
Mybatis缓存原理:
只要开启了二级缓存,在同一个Mapper下就有效,所有的数据都会先放在一级缓存中,只有当会话提交,或者关闭的时候,才会提交到二级缓存中,图解如下所示