系列博客目录:MyBatis从零开始博客目录
6. MyBatis缓存配置
使用缓存可以使应用更快地获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis作为持久化框架,提供了非常强大的查询缓存特性,可以非常方便地配置和定制使用。
MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存。
默认情况下,只有一级缓存开启(SqlSession级别的缓存,也称为本地缓存);
二级缓存需要手动开启和配置,他是基于namespace级别的缓存;
为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存。
6.1 一级缓存
一级缓存也叫本地缓存:SqlSession
与数据库同义词回话期间查询得到的数据会放在本地缓存中。
以后如果需要获取相同的数据,直接从缓存中拿,没有必要再去查询数据库。
测试代码如下:
CacheTest.java
package com.xiangty.test;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import com.xiangty.bean.User;
import com.xiangty.mapper.UserMapper;
public class CacheTest {
@Test
public void testL1Cache() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
System.out.println("=============================================");
User user2 = userMapper.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession.close();
}
}
}
输出结果:
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
User [id=1, username=管理员, password=123456]
true
如上代码,发现在同一个SqlSession回话中,getUserById方法查询相同id的信息,查询出来的对象是相等的,而且日志中可以看到查询的SQL语句只有一个,说明两次查询存在缓存,第二次查询用到了第一次查询的结果。
缓存失效的情况:
1.查询不同的东西;
2.增删改操作,可以能会改变原来的数据,所以必定会刷新缓存;
3.查询不同的Mapper;
4.手动清理缓存。
Cache.java部分代码实例如下:
/**
* 查询不同的东西
*/
@Test
public void testL2Cache() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// id=1
User user = userMapper.getUserById(1);
System.out.println(user);
System.out.println("=============================================");
// id=2
User user2 = userMapper.getUserById(2);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession.close();
}
}
运行结果:
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 2(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 2, 路人甲, 123456
DEBUG [main] - <== Total: 1
User [id=2, username=路人甲, password=123456]
false
/**
* 增删改操作,可以能会改变原来的数据,所以必定会刷新缓存;
*/
@Test
public void testL3Cache() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
User deleteUser = new User();
deleteUser.setUsername("password1");
userMapper.deleteByUsername(deleteUser);
sqlSession.commit();
System.out.println("=============================================");
User user2 = userMapper.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession.close();
}
}
运行结果:
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
DEBUG [main] - ==> Preparing: DELETE FROM user WHERE username LIKE CONCAT('%',?,'%')
DEBUG [main] - ==> Parameters: password1(String)
DEBUG [main] - <== Updates: 5
=============================================
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
false
/**
* 手动清除缓存
*/
@Test
public void testL4Cache() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
sqlSession.clearCache(); // 清除缓存
System.out.println("=============================================");
User user2 = userMapper.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession.close();
}
}
运行结果:
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
false
小结:一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接这段时间区间。
6.2 二级缓存
6.2.1 二级缓存配置
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存。
基于namespace级别的缓存,一个名称空间,对应一个二级缓存(但是前提是操作必须在同一个SqlSessionFactory 中进行)。
工作机制:
一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
如果当前会话关闭了,这个会话对应的一级缓存就没有了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
新的会话查询信息,就可以从二级缓存中获取内容;
不同的mapper查出的数据会放在自己对应的缓存中。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
可以通过 cache 元素的属性来修改。比如:
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
测试代码如下:
mybatis-config.xml 新增setting配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 显示开启全局缓存 -->
<setting name="cacheEnabled" value="true"></setting>
</settings>
</configuration>
UserMapper.xml新增
<cache/>
也可以自定一些参数如下:
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
User.java 序列化
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = -6730295119137974064L;
// 省略属性、get和set方法等方法
}
SqlSessionUtil.java 新增getSqlSessionFactory方法
public static SqlSessionFactory getSqlSessionFactory() {
// 此处可以不使用try抛异常,SqlSessionFactoryBuilder().build()方法中有reader流关闭的操作
try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml");) {
return new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
CacheTest.java测试代码如下:
/**
* 测试二级缓存
*/
@Test
public void testL6Cache() {
SqlSessionFactory sqlSessionFactory = SqlSessionUtil.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
System.out.println(sqlSession==sqlSession2);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
sqlSession.close();
System.out.println("=============================================");
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession2.close();
}
}
运行结果:
false
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.5
User [id=1, username=管理员, password=123456]
true
6.2.2 解决二级缓存不生效问题
以6.2.1章节的配置和代码为基础:
SqlSessionUtil.java如下:
public class SqlSessionUtil {
public static SqlSession getSqlSession() {
SqlSession sqlSession = null;
// 此处可以不使用try抛异常,SqlSessionFactoryBuilder().build()方法中有reader流关闭的操作
try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml");) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
sqlSession = sqlSessionFactory.openSession();
} catch (IOException e) {
e.printStackTrace();
}
if (sqlSession != null) {
return sqlSession;
} else {
throw new NullPointerException("sqlSession为空");
}
}
}
CachedTest.java
@Test
public void testL5Cache() {
// 不在同一个SqlSessionFactory是没有办法实现二级缓存的
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
SqlSession sqlSession2 = SqlSessionUtil.getSqlSession();
System.out.println(sqlSession==sqlSession2);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
sqlSession.close();
System.out.println("=============================================");
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession2.close();
}
}
运行结果:
false
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
false
问题描述:
在MyBatis中,不同SqlSession作用域中开启了两个相同的查询操作。但是在控制台的输出中一直显示没有命中缓存,持续进行SQL查询操作。
问题排查:
1.是否在mybatis-conifg.xml中开启了全局缓存的设置。
2.是否在mapper.xml文件中打开了cache设置。
3.Bean对象是否可以序列化
上述步骤才可以打开二级缓存,现在在控制台中已经有Cache Hit Ratio 0的输出,证明二级缓存已经打开,但是由于SQL的相关问题,导致的缓存没有命中。继续排查……
4.SQL 语句是否一致
5.是否在新的SQL查询之前没有关闭上一个SqlSession,导致命中了一级缓存而不需要查询二级缓存
最终结论:
二级缓存存在于 SqlSessionFactory 生命周期中
虽然二级缓存针对的对象是一个Mapper对象,只要是针对同一个Mapper的操作,都可以实现二级缓存。但是前提是操作必须在同一个SqlSessionFactory 中进行。
在测试的时候,SqlSessionUtil.getSqlSession()每次都创建了一个SqlSessionFactory,实例化了多个SqlSessionFactory这样在后续的SQL操作中,是不可能命中二级缓存的。
代码修改如下:
SqlSessionUtil.java 新增getSqlSessionFactory方法
public static SqlSessionFactory getSqlSessionFactory() {
// 此处可以不使用try抛异常,SqlSessionFactoryBuilder().build()方法中有reader流关闭的操作
try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml");) {
return new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
CacheTest.java测试代码如下:
/**
* 测试二级缓存
*/
@Test
public void testL6Cache() {
SqlSessionFactory sqlSessionFactory = SqlSessionUtil.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
System.out.println(sqlSession==sqlSession2);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
sqlSession.close();
System.out.println("=============================================");
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUserById(1);
System.out.println(user2);
System.out.println(user == user2);
} finally {
sqlSession2.close();
}
}
运行结果:
false
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT id, username, password FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, username, password
TRACE [main] - <== Row: 1, 管理员, 123456
DEBUG [main] - <== Total: 1
User [id=1, username=管理员, password=123456]
=============================================
DEBUG [main] - Cache Hit Ratio [com.xiangty.mapper.UserMapper]: 0.5
User [id=1, username=管理员, password=123456]
true
问题解决。
如果文档中有任何问题,可以直接联系我,便于我改正和进步。希望文档对您有所帮助。文档中代码GitHub地址:https://gitee.com/xiangty1/learn-MyBatis/