MyBatis缓存教程

MyBatis 提供了一级缓存和二级缓存的支持,用于提高数据库查询的性能,减少不必要的数据库访问。

一级缓存(SqlSession 级别的缓存)

一级缓存是 MyBatis 中最细粒度的缓存,也称为本地缓存。它存在于每个 SqlSession 的生命周期中,当 SqlSession 被关闭或清空时,该缓存就会被清空。

什么是SqlSession?
SqlSession是Java程序和数据库之间的会话。

作用范围:同一个 SqlSession 中。
举例:

@Test
public void test1() {
	//获取SqlSession
	SqlSession sqlSession = SqlSessionUtil.getSqlSession();
	CacheMapper mapper = sqlSession.getMapper(CacheMapper.class);
	//首次获取SqlSession中的数据
	List<Emp> emps1 = mapper.getEmpByDeptId(2);
	//第二次获取SqlSession中的数据
	List<Emp> emps2 = mapper.getEmpByDeptId(2);
	emps1.forEach(System.out::println);
	emps2.forEach(System.out::println);
	sqlSession.close();
}

输出结果:

image

结论:
在同一个SqlSession中,可以看出SQL执行了一次,第二次获取数据是从缓存中获取。

一级缓存失效的四种情况

  1. 不同的 SqlSession 之间,缓存数据互不共享。
    举例:
@Test
public void test1() {
	//获取SqlSession
	SqlSession sqlSession1 = SqlSessionUtil.getSqlSession();
	CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
	List<Emp> emps1 = mapper1.getEmpByDeptId(2);
	emps1.forEach(System.out::println);

	//获取SqlSession
	SqlSession sqlSession2 = SqlSessionUtil.getSqlSession();
	CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
	List<Emp> emps2 = mapper2.getEmpByDeptId(2);
	emps2.forEach(System.out::println);

	//关闭SqlSession
	sqlSession1.close();
	sqlSession2.close();
}

输出结果:

image

结论:
从结果可以看出,不同的SqlSession对应不同的一级缓存数据。

  1. 同一个 SqlSession 中,执行了任何一次增删改操作(即涉及到了数据库写操作)
    举例:
@Test
public void test1() {
	//获取SqlSession
	SqlSession sqlSession1 = SqlSessionUtil.getSqlSession();
	CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
	//第一次获取SqlSession中的数据
	List<Emp> emps1 = mapper1.getEmpByDeptId(2);
	emps1.forEach(System.out::println);

	//执行添加操作
	int result = mapper1.addEmp(new Emp(0, "evan", 20, "女", null));
	System.out.println("添加结果:" + result);

	//第二次获取SqlSession中的数据
	List<Emp> emps2 = mapper1.getEmpByDeptId(2);
	emps2.forEach(System.out::println);

	//关闭SqlSession
	sqlSession1.close();
}

输出结果:

image

结论:
从结果可以看出,在同一个SqlSession两次执行查询操作期间执行了一次添加操作,导致缓存失效,第二次执行查询操作时,重新获取SqlSession创建缓存。

  1. 同一个SqlSession两次查询期间手动清空了缓存
    举例:
@Test
public void test1() {
	//获取SqlSession
	SqlSession sqlSession1 = SqlSessionUtil.getSqlSession();
	CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
	//第一次获取SqlSession中的数据
	List<Emp> emps1 = mapper1.getEmpByDeptId(2);
	emps1.forEach(System.out::println);

	//清空缓存
	sqlSession1.clearCache();

	//第二次获取SqlSession中的数据
	List<Emp> emps2 = mapper1.getEmpByDeptId(2);
	emps2.forEach(System.out::println);

	//关闭SqlSession
	sqlSession1.close();
}

输出结果:

image

结论:
同一个SqlSession两次查询期间手动清空了缓存,导致缓存失效。

  1. 同一个SqlSession但是查询条件不同
    举例:
@Test
public void test1() {
	//获取SqlSession
	SqlSession sqlSession1 = SqlSessionUtil.getSqlSession();
	CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
	//第一次获取SqlSession中的数据
	List<Emp> emps1 = mapper1.getEmpByDeptId(2);
	emps1.forEach(System.out::println);

	//第二次获取SqlSession中的数据
	List<Emp> emps2 = mapper1.getEmpByDeptId(3);
	emps2.forEach(System.out::println);

	//关闭SqlSession
	sqlSession1.close();
}

输出结果:

image

**结论:
从结果可以看出,同一个SqlSession查询不同数据,导致缓存失效。

二级缓存(SqlSessionFactory 级别的缓存)

二级缓存是跨 SqlSession 的,其生命周期与 Mapper 映射文件的命名空间相关。它可以被多个 SqlSession 共享,通常用于缓存查询结果,以减少对数据库的访问。
通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中。

获取二级缓存开启的条件:

  • 在核心配置文件中,设置全局配置属性cacheEnabled="true",默认为true,不需要设置
<!-- 全局配置 -->
<settings>
    <!-- 默认开启的,可以显式设置 -->
    <setting name="cacheEnabled" value="true"/>
</settings>
  • 在映射文件中设置标签<cache/>
<?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.evan.mybatis.mapper.CacheMapper">
    <!-- 设置二级缓存 -->
    <cache/>
</mapper>
  • 二级缓存必须在SqlSession关闭或提交之后有效
private static final Log logger = LogFactory.getLog(CacheTest.class);

@Test
public void test2() {
	try {
		//读取MyBatis核心配置文件中的信息
		InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
		//构建SqlSessionFactory对象
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
		//使用SqlSessionFactory创建SqlSession对象
		SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
		CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
		System.out.println(mapper1.getEmpByDeptId(1));
		sqlSession1.close();

		SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
		CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
		System.out.println(mapper2.getEmpByDeptId(1));
		sqlSession2.close();
	} catch (IOException e) {
		logger.error(e);
	}
}

输出结果:

image

从结果可以看出,首次获取完SqlSession中的数据,关闭SqlSession;当第二次获取时,出现异常:Error serializing object. Cause: java.io.NotSerializableException: com.evan.mybatis.entity.Emp,这个异常是说查询数据的实体类没有实现序列化。

  • 查询的数据所转换的实体类类型必须实现序列化的接口
public class Emp implements Serializable {
    
    //声明唯一序列号
    private static final long serialVersionUID = -4262890992L;
}

最终测试

public class CacheTest {

    private static final Log logger = LogFactory.getLog(CacheTest.class);

    @Test
    public void test2() {
        try {
            //读取MyBatis核心配置文件中的信息
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            //构建SqlSessionFactory对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //通过SqlSessionFactory创建SqlSession对象
            SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
            SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

            CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
            List<Emp> emps1 = mapper1.getEmpByDeptId(1);
            emps1.forEach(System.out::println);
            //关闭sqlSession的时候才会把保存在一级缓存中的数据保存到二级缓存中
            sqlSession1.close();

            CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
            List<Emp> emps2 = mapper2.getEmpByDeptId(1);
            emps2.forEach(System.out::println);
            //关闭sqlSession的时候才会把保存在一级缓存中的数据保存到二级缓存中
            sqlSession2.close();
        } catch (IOException e) {
            logger.error(e);
        }
    }
}

输出结果:

image

首次执行查询操作获取数据,第二次从二级缓存中获取的数据。

二级缓存失效的情况

同一个SqlSessionFactory下的SqlSession两次查询相同SQL之间执行了任意的增删改操作,会使一级和二级缓存同时失效。

public class CacheTest {

    private static final Log logger = LogFactory.getLog(CacheTest.class);

    @Test
    public void test2() {
        try {
            //读取MyBatis核心配置文件中的信息
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            //通过SqlSessionFactory创建SqlSession对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
            SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

            CacheMapper mapper1 = sqlSession1.getMapper(CacheMapper.class);
            List<Emp> emps1 = mapper1.getEmpByDeptId(1);
            emps1.forEach(System.out::println);
            
            //执行删除操作,缓存失效
            System.out.println(mapper1.deleteEmp(13));

            //关闭sqlSession的时候才会把保存在一级缓存中的数据保存到二级缓存中
            sqlSession1.close();

            CacheMapper mapper2 = sqlSession2.getMapper(CacheMapper.class);
            List<Emp> emps2 = mapper2.getEmpByDeptId(1);
            emps2.forEach(System.out::println);
            //关闭sqlSession的时候才会把保存在一级缓存中的数据保存到二级缓存中
            sqlSession2.close();
        } catch (IOException e) {
            logger.error(e);
        }
    }
}

输出结果:

image

二级缓存的相关配置

在mapper配置文件中添加的cache标签可以设置一些属性:
eviction属性:缓存回收策略

  • LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
  • FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。

flushInterval属性:缓存自动刷新的时间间隔,单位毫秒。

  • 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用SQL语句时刷新

size属性:引用数目,正整数。

  • 代表缓存最多可以存储多少个对象,太大容易导致内存溢出

readOnly属性:只读,true/false

  • true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
  • false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
<!-- 设置二级缓存 -->
<cache>
	<property name="eviction" value="LRU"/> <!-- 缓存回收策略 -->
	<property name="flushInterval" value="5000"/> <!-- 自动刷新时间间隔 -->
	<property name="size" value="1024"/>  <!-- 最大缓存1024g个引用对象 -->
	<property name="readOnly" value="true"/> <!-- 只读 -->
</cache>

重用cache标签配置

在命名空间中共享相同的缓存配置和实例,可以使用cache-ref 元素来引用另外一个缓存。
例如:

<!--需要指定Mapper映射文件的位置-->
<cache-ref namespace="com.evan.mapper.DeptMapper"/>

MyBatis缓存查询的顺序

执行查询操作时:

  • 先查询二级缓存,因为二级缓存中可能会有其他sqlSession已经查出来的数据,这样可以直接获取。
  • 如果二级缓存没有查询的数据,再查询一级缓存
  • 如果一级缓存也没有查询的数据,则查询数据库
  • SqlSession关闭之后,一级缓存中的数据会写入二级缓存

相关文章

1.pom.xml引入依赖 &lt;dependency&gt; &lt;gro...
&lt;?xml version=&quot;1.0&quot; encoding=&a...
准备工作 ① 创建数据库&amp;数据表 ## 创建数据库 CREA...
MyBatis逆向工程是指根据数据库表结构自动生成对应的实体类、...
MyBatis获取参数值的两种方式:${}和#{} ${}的本质就是字符串...
resultMap作用是处理数据表中字段与java实体类中属性的映射关...