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();
}
输出结果:
结论:
在同一个SqlSession中,可以看出SQL执行了一次,第二次获取数据是从缓存中获取。
一级缓存失效的四种情况
- 不同的 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();
}
输出结果:
结论:
从结果可以看出,不同的SqlSession对应不同的一级缓存数据。
- 同一个 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();
}
输出结果:
结论:
从结果可以看出,在同一个SqlSession两次执行查询操作期间执行了一次添加操作,导致缓存失效,第二次执行查询操作时,重新获取SqlSession创建缓存。
- 同一个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();
}
输出结果:
结论:
同一个SqlSession两次查询期间手动清空了缓存,导致缓存失效。
- 同一个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();
}
输出结果:
**结论:
从结果可以看出,同一个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);
}
}
输出结果:
从结果可以看出,首次获取完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);
}
}
}
输出结果:
首次执行查询操作获取数据,第二次从二级缓存中获取的数据。
二级缓存失效的情况
同一个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);
}
}
}
输出结果:
二级缓存的相关配置
在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关闭之后,一级缓存中的数据会写入二级缓存