mybatis缓存解读及基于springBoot的代码验证(超详细)
一、mybatis缓存简介
mybatis 包含了一个非常强大的查询缓存特性,它可以非常方便地配置和定制,共有两级缓存机制。
当程序通过mybatis发起查询命令,mybatis则会根据程序发送的命令首先去缓存中寻找,如果命中缓存,则直接将缓存中的数据返回,否则,则去数据库查询。
值得一提的是,mybatis会首先去二级缓存中查找,如为命中,再去一级缓存中查找,最后才会进入数据库进行查询。
同时,当执行update,delete,insert操作时,会清空缓存。
1、 一级缓存
操作数据库时,我们需要构造 sqlSession对象,而sqlSession使用了一个HashMap的数据结构去存储缓存数据。
一级缓存是SqlSession级别的缓存,默认开启。即表示其作用域为同一个sqlsession中。
上面我们已经通过一张图了解了从程序到缓存到数据库的流程,那么什么情况下mybatis会命中一级缓存呢?又或者说,命中一级缓存需要什么条件?
命中一级缓存共需要以下几个条件:
- 相同的入参(sql传入的参数一样);
- 同一个会话(即为同一个sqlSession,一级缓存为sqlSession级别的缓存);
- 相同的Statement ID(即同一个方法,相同的方法名,Mapper文件中相同的namespace,相同的sql id);
- 相同的rowbound(行范围一样);
2、 二级缓存
二级缓存比一级缓存范围更大,其为Mapper级别的缓存。只要在同一个namespace中,即便跨越了不同的sqlsession,也可实现命中二级缓存。需要注意的是,当一个sqlsession执行完毕查询后,需要执行commit操作或close操作,才可把数据写入缓存中。接下来我们用一张图来说明二级缓存的范围:
二级缓存默认是关闭的,需要我们手动开启,接下来看如何配置开启二级缓存:
- 首先在配置文件中开启mybatis二级缓存,此处我以springboot配置文件yml格式为例:将cache-enabled属性设置为true即可
mybatis:
#配置开启二级缓存
configuration:
cache-enabled: true
- 接下来,我们需要在Mapper文件中设置开启二级缓存
<mapper namespace="">
<!--开启二级缓存-->
<cache></cache>
<update id=""></update>
<select id=""></select>
</mapper>
- 最后,实体类继承Serializable,以便反序列化
public class Student implements Serializable {
...
...
...
}
如此即可开启二级缓存。
二、基于springboot的代码验证
1、 测试工程说明
该工程基于springboot框架,为了方便我们阅读,我配置了最简单的springboot,整合了mybatis,并设置日志输出级别为debug,方便我们观察程序向数据库发送sql命令的情况。同时我只书写了dao层、entry层及mapper文件。以下我们一个个先来看以下整个测试工程:
1.1、数据库
数据库非常简单,一张student表,id,name,age,sex四个字段,我在这里插入了3条数据
1.2、配置文件
配置文件采用application.yml格式,此处如需开启二级缓存,则配置cache-enabled为 true
#端口号
server:
port: 8080
spring:
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/student?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
#mybatis配置
mybatis:
mapper-locations: mappers/**/*.xml
#配置开启二级缓存
#configuration:
# cache-enabled: true
#设置日志级别为debug,方便我们查看控制台打印sql(原谅我这里层级有点多哈哈哈)
logging:
level:
com:
example:
mybatis_cache:
student:
dao:
StudentMapper: debug
1.3、entry、dao及mapper
entry很简单,只有一个实体类,student类,与数据库相呼应。
package com.example.mybatis_cache.student.entry;
import java.io.Serializable;
/**
* 实体类
* @author zhaiLiMing
*/
public class Student implements Serializable {
private int id;
private String name;
private int age;
private String sex;
public int getId() { return id;}
public void setId(int id) { this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
public String getSex() {return sex;}
public void setSex(String sex) {this.sex = sex;}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
dao相对应有一个studentMapper,共有两个方法
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* student dao
* @author zhaiLiMing
*/
@Mapper
public interface StudentMapper {
//根据id查询
public Student getById(@Param("id")int id);
//修改
public int updateById(Student student);
}
mapper则与dao相对应
<?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.example.mybatis_cache.student.dao.StudentMapper">
<!--配置开启二级缓存-->
<!--<cache></cache>-->
<update id="updateById">
update student set name = #{name} where id = #{id}
</update>
<select id="getById" resultType="com.example.mybatis_cache.student.entry.Student">
select * from student where id= #{id}
</select>
</mapper>
至此整个测试工程介绍完毕,可以看到,我们只有entry、dao、mapper,而没有写service和controller,因为下面用不上。
2、 一级缓存命中测试
2.1、sqlsession范围测试
命中缓存,即直接从缓存中拿到数据,不访问数据库,我们先看一下测试代码
package com.example.mybatis_cache;
import com.example.mybatis_cache.student.dao.StudentMapper;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
/**
* 单元测试
* @author zhaiLiMing
*/
@SpringBootTest
class MybatisCacheApplicationTests {
@Resource
SqlSessionFactory sqlSessionFactory;
/**
* 一级缓存sqlsession范围测试
*/
@Test
void firstCache_sqlsession() {
//开启第一个Sqlsession s1
SqlSession s1=sqlSessionFactory.openSession();
StudentMapper stu1=s1.getMapper(StudentMapper.class);
//开启第二个Sqlsession s2
SqlSession s2=sqlSessionFactory.openSession();
StudentMapper stu2=s2.getMapper(StudentMapper.class);
//s1执行第一次查询操作
System.out.println("first sqlSession : "+stu1.getById(1).toString());
//s1执行第二次查询操作
System.out.println("first sqlSession : "+stu1.getById(1).toString());
//s2执行查询操作
System.out.println("second sqlSession : "+stu2.getById(1).toString());
}
}
在以上代码中,开启了两个sqlsession,首先s1执行了两次查询操作,然后s2执行了一次查询操作,流程如下
- s1执行第一次查询
- s1执行第二次查询
- s2执行查询
那么按照理论,我们可预测出以下结果:
s1第一次查询未命中缓存,访问数据库;
s1第二次查询因符合命中条件,命中了缓存;
s2查询,超出了同一个sqlsession范围,未命中缓存,访问了数据库;
接下来看一下运行后控制台的输出内容
可以看到,超出sqlsession范围之后,的确未命中缓存。
2.2、update清空缓存
我们知道,当执行当update,delete,insert操作时,会清空缓存,在此,我们进行一个updata的测试,下面我贴出这个测试方法。
/**
* 一级缓存update清空缓存测试
*/
@Test
void firstCache_update() {
//实例化一个对象
Student student = new Student();
student.setId(1);
student.setName("小明");
//开启Sqlsession s1
SqlSession s1=sqlSessionFactory.openSession();
StudentMapper stu1=s1.getMapper(StudentMapper.class);
//先进行两次查询,验证命中缓存
System.out.println("first select before update : "+stu1.getById(1).toString());
System.out.println("second select before update : "+stu1.getById(1).toString());
//执行update,把id为1的学生姓名改为小明
stu1.updateById(student);
//再用同一个sqlsession进行相同的查询操作
System.out.println("third select after update : "+stu1.getById(1).toString());
}
以上代码进行了如下操作:
- s1执行第一次查询
- s1执行第二次查询
- s1执行update修改数据
- s1执行第三次查询
由已知结论,我们可预测以下结果:
s1执行第一次查询未命中缓存,访问数据库;
s1执行第二次查询,符合命中条件,命中缓存;
s1执行第三次查询,因之前执行了update操作,清空了缓存,所以未命中缓存,访问数据库。
然后看一下控制台输出的情况。
可以看到,update操作之后,即使在同一个sqlsession再执行相同的操作,依旧访问了数据库,可证明缓存被清空了。
3、二级缓存命中测试
3.1、跨sqlsession的namespace范围测试
二级缓存是跨越sqlsession的,我在上面的工程中,开启了二级缓存,接下来我们测试一下不同的sqlsession在同一个namespace中的表现。
/**
* 二级缓存同mapper不同sqlsession测试
*/
@Test
void secondCache_mapper() {
//开启第一个Sqlsession s1
SqlSession s1=sqlSessionFactory.openSession();
StudentMapper stu1=s1.getMapper(StudentMapper.class);
//开启第二个Sqlsession s2
SqlSession s2=sqlSessionFactory.openSession();
StudentMapper stu2=s2.getMapper(StudentMapper.class);
//开启第三个Sqlsession s3
SqlSession s3=sqlSessionFactory.openSession();
StudentMapper stu3=s3.getMapper(StudentMapper.class);
//s1开始执行select操作,且不提交,不关闭
System.out.println("first sqlSession : "+stu1.getById(1).toString());
//s2开始执行select操作,执行完毕后执行commit操作
System.out.println("second sqlSession : "+stu2.getById(1).toString());
//s2执行commit操作,此处执行close也是相同效果
s2.commit();
//s3开始执行查询
System.out.println("third sqlSession : "+stu3.getById(1).toString());
}
以上代码中,首先进行了s1查询,不提交,不关闭,然后进行s2查询,执行提交,再进行s3查询。所用都是相同Mapper,同一个namespace,流程如下:
- s1执行查询
- s2执行查询
- s2提交
- s3执行查询
那么根据之前的理论,我们可预测结果以下几点:
s1因为第一次查询,未命中缓存,访问数据库;
s2虽然是第二次查询,但因s1执行查询后为提交,所以未命中缓存,访问数据库;
s3为第三次查询,因为s2执行commit提交了数据,所以命中缓存;
接下来看一下结果:
可以看到,虽然超出了同一个sqlsession的范围,但只要保证相同的namespace,就可命中二级缓存。
3.2、update清空缓存
一级缓存已经进行了update的操作,接下来二级缓存我们进行同样的操作,只不过把范围修改为同namespace不同sqlsession。下面来看代码:
/**
* 二级缓存update清空缓存测试
*/
@Test
void secondCache_update() {
//实例化一个对象
Student student = new Student();
student.setId(1);
student.setName("小红");
//开启第一个Sqlsession s1
SqlSession s1=sqlSessionFactory.openSession();
StudentMapper stu1=s1.getMapper(StudentMapper.class);
//开启第二个Sqlsession s2
SqlSession s2=sqlSessionFactory.openSession();
StudentMapper stu2=s2.getMapper(StudentMapper.class);
//开启第三个Sqlsession s3
SqlSession s3=sqlSessionFactory.openSession();
StudentMapper stu3=s3.getMapper(StudentMapper.class);
//sqlsession1执行查询,并执行commit把数据写入二级缓存
System.out.println("sqlsession1 select before update : "+stu1.getById(1).toString());
//sqlsession1执行commit,把数据写入二级缓存
s1.commit();
//sqlsession2执行查询
System.out.println("sqlsession2 select before update : "+stu2.getById(1).toString());
//保证同namespace,任意一个sqlsession执行update,把id为1的学生姓名改为小红,此处用尚未使用的sqlsession3
stu3.updateById(student);
//s3执行提交,但即使不提交,也不影响清空缓存
s3.commit();
//sqlsession3执行查询
System.out.println("sqlsession3 select after update : "+stu3.getById(1).toString());
}
以上代码中,首先使用s1进行了查询,并执行了提交,接下来使用s2进行查询,然后用s3执行update把学生姓名修改为小红,再用s3执行了提交,然后用s3进行了查询,流程如下:
- s1进行查询
- s1执行commit
- s2进行查询
- s3执行update修改数据
- s3进行查询
根据之前的理论,可预测以下结果:
s1因为第一次查询,未命中缓存,访问数据库;
s2为第二次查询,因为s1执行commit提交了数据,所以命中缓存;
s3为第三次查询,虽然s1执行了commit,但在s3查询之前,当前mapper中有sqlsession(s3)执行了update,清空了缓存,所以未命中缓存;
接下来看一下结果:
可以看到,当执行update操作之后,二级缓存被清空了。