淘先锋技术网

首页 1 2 3 4 5 6 7

一、mybatis缓存简介

mybatis 包含了一个非常强大的查询缓存特性,它可以非常方便地配置和定制,共有两级缓存机制。

当程序通过mybatis发起查询命令,mybatis则会根据程序发送的命令首先去缓存中寻找,如果命中缓存,则直接将缓存中的数据返回,否则,则去数据库查询。

命中缓存
未命中缓存

值得一提的是,mybatis会首先去二级缓存中查找,如为命中,再去一级缓存中查找,最后才会进入数据库进行查询。

同时,当执行update,delete,insert操作时,会清空缓存。

1、 一级缓存

操作数据库时,我们需要构造 sqlSession对象,而sqlSession使用了一个HashMap的数据结构去存储缓存数据。

一级缓存是SqlSession级别的缓存,默认开启。即表示其作用域为同一个sqlsession中。

上面我们已经通过一张图了解了从程序到缓存到数据库的流程,那么什么情况下mybatis会命中一级缓存呢?又或者说,命中一级缓存需要什么条件?

命中一级缓存共需要以下几个条件:

  1. 相同的入参(sql传入的参数一样);
  2. 同一个会话(即为同一个sqlSession,一级缓存为sqlSession级别的缓存);
  3. 相同的Statement ID(即同一个方法,相同的方法名,Mapper文件中相同的namespace,相同的sql id);
  4. 相同的rowbound(行范围一样);

2、 二级缓存

二级缓存比一级缓存范围更大,其为Mapper级别的缓存。只要在同一个namespace中,即便跨越了不同的sqlsession,也可实现命中二级缓存。需要注意的是,当一个sqlsession执行完毕查询后,需要执行commit操作或close操作,才可把数据写入缓存中。接下来我们用一张图来说明二级缓存的范围:

mybatis二级缓存示意图
二级缓存默认是关闭的,需要我们手动开启,接下来看如何配置开启二级缓存:

  1. 首先在配置文件中开启mybatis二级缓存,此处我以springboot配置文件yml格式为例:将cache-enabled属性设置为true即可
mybatis:
  #配置开启二级缓存
  configuration:
    cache-enabled: true
  1. 接下来,我们需要在Mapper文件中设置开启二级缓存
<mapper namespace="">

    <!--开启二级缓存-->
	<cache></cache>
	
	<update id=""></update>
	
	<select id=""></select>

</mapper>
  1. 最后,实体类继承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执行了一次查询操作,流程如下

  1. s1执行第一次查询
  2. s1执行第二次查询
  3. s2执行查询

那么按照理论,我们可预测出以下结果:
s1第一次查询未命中缓存,访问数据库;
s1第二次查询因符合命中条件,命中了缓存;
s2查询,超出了同一个sqlsession范围,未命中缓存,访问了数据库;

接下来看一下运行后控制台的输出内容
sqlsession范围测试log
可以看到,超出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());
    }

以上代码进行了如下操作:

  1. s1执行第一次查询
  2. s1执行第二次查询
  3. s1执行update修改数据
  4. s1执行第三次查询

由已知结论,我们可预测以下结果:
s1执行第一次查询未命中缓存,访问数据库;
s1执行第二次查询,符合命中条件,命中缓存;
s1执行第三次查询,因之前执行了update操作,清空了缓存,所以未命中缓存,访问数据库。

然后看一下控制台输出的情况。
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,流程如下:

  1. s1执行查询
  2. s2执行查询
  3. s2提交
  4. s3执行查询

那么根据之前的理论,我们可预测结果以下几点:

s1因为第一次查询,未命中缓存,访问数据库;
s2虽然是第二次查询,但因s1执行查询后为提交,所以未命中缓存,访问数据库;
s3为第三次查询,因为s2执行commit提交了数据,所以命中缓存;

接下来看一下结果:
二级缓存同Mapper不同sqlsession测试log
可以看到,虽然超出了同一个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进行了查询,流程如下:

  1. s1进行查询
  2. s1执行commit
  3. s2进行查询
  4. s3执行update修改数据
  5. s3进行查询

根据之前的理论,可预测以下结果:

s1因为第一次查询,未命中缓存,访问数据库;
s2为第二次查询,因为s1执行commit提交了数据,所以命中缓存;
s3为第三次查询,虽然s1执行了commit,但在s3查询之前,当前mapper中有sqlsession(s3)执行了update,清空了缓存,所以未命中缓存;

接下来看一下结果:
二级缓存清空缓存log
可以看到,当执行update操作之后,二级缓存被清空了。