简要说明:
1、Mybatis缓存分为一级缓存和二级缓存。在没有配置的情况下,默认开启一级缓存,不开启二级缓存。
2、如果配置开启二级缓存,会先查询二级缓存,没有的话再查询一级缓存。(原理)
(如果是springboot项目,默认mybatis.configuration.cache-enabled的值是true,也就是默认开启Mybatis的二级缓存的,但也需要相应的配置才能使用二级缓存)
一级缓存(同一个SqlSession)
一级缓存具有和sqlsession一样的生命周期,SqlSession相当于一个JDBC的Connection对象,在一次请求事务会话后,我们将其关闭。
import com.boot.mybatis.dao.UserMapper;
import com.boot.mybatis.po.User;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
* @author river
* 2020/2/11
*/
@SpringBootTest
class MybatisCacheTest {
@Autowired
SqlSessionFactory sqlSessionFactory;
@Test
void theSameSqlSession() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {//在try()中的资源会自动释放
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//查询结果会缓存起来,如果不希望缓存这个查询结果,可以在<select>中加上flushCache="true",或者任何的update, insert, delete语句都会清空此缓存。
User user1 = userMapper.selectByPrimaryKey(1);
//下面不会查询数据库,第一次查询的结果缓存在sqlSession一级缓存中
User user2 = userMapper.selectByPrimaryKey(1);
assertEquals(user1, user2);//同一个User对象,地址相同
}
}
@Test
void notTheSameSqlSession(){
SqlSession sqlSession2 = null;
try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
//查询结果会缓存到sqlSession1中
User user1 = userMapper1.selectByPrimaryKey(1);
sqlSession1.commit();//一定要提交,不然不会进入二级缓存
sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
//sqlSession2中没有缓存此查询,所以会查询数据库(没有配置二级缓存的时候)
User user2 = userMapper2.selectByPrimaryKey(1);
assertNotEquals(user1, user2);
// assertEquals(user1, user2);//如果配置了readOnly="true",则获取到的是同一个User对象,地址相同
}finally {
if (null != sqlSession2) {
sqlSession2.close();
}
}
}
}
为什么需要二级缓存?
一级缓存的问题在于:
1、相同的Mapper类,相同的sql语句,不同的SqlSession却会查询数据库两次(如notTheSameSqlSession测试)。为了解决这个问题,所以就需要二级缓存,它的作用于范围就是Mapper级别的。这样,使用缓存可以在SqlSessionFactory层面上能提供给各个SqlSession共享。
2、Spring与Mybatis整合时,在未开启事务的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的。
什么时候二级缓存会被清除?
如果调用相同namespace下的mapper映射文件中的增删改SQL(insert、update、delete),并执行了commit操作。此时会清空该namespace下的二级缓存。
开启二级缓存的条件是什么?
缓存对象必须是可序列化的,也就是实现Serializable接口,因为二级缓存不一定是缓存到内存中的,它的存储介质多种多样。
二级缓存的生命周期
二级缓存的生命周期同SqlSessionFactory。 SqlSessionFactory一般是一个全局单例,对应一个数据库连接池,应用启动后就一直存在。SqlSessionFactory是由SqlSessionFactoryBuilder通过XML或者Java编码来构建的。
配置二级缓存
很简单,一个标签<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.boot.mybatis.dao.UserMapper">
<cache />
<resultMap ...>
...
</mapper>
这个标签<cache />默认设置了如下特性:(源自:《深入浅出Mybatis技术原理与实战》)
1、包含<cache />标签的mapper文件中所有select语句都将会被缓存;
2、包含<cache />标签的mapper文件中所有insert、update和delete都会刷新缓存;
3、缓存会使用默认的Least Recently Used(LRU,最近最少使用算法)来进行回收;
4、缓存不会自动刷新。(可以设置每隔多少时间自动刷新)
5、缓存一共可以缓存1024个对象的引用(列表、集合或者对象)
6、该缓存是可读/可写缓存(read/write),对象引用不是共享的,对象引用可以安全的被调用者修改。也就是每次查询出来的对象内存地址不同,内容相同(如果没有重载equals方法,则调用这个方法会返回false)。如果查询出来的对象,修改了相关属性,后续查询出来的对象的属性也是修改后的值。
设置二级缓存的属性
<cache
eviction="LRU"
flushInterval="100000"
size="512"
readOnly="true"
/>
eviction:表示缓存回收策略,有如下几种:
1、LRC:最近最少使用策略,移除最长时间不使用的对象。
2、FIFO:先进先出策略,顾名思义。
3、SOFT:软引用,移除基于垃圾回收器的状态和软引用规则的对象。
4、WEAK:弱引用,更积极地移除基于垃圾回收期的状态和弱引用规则的对象。
flushInterval:刷新间隔时间,单位为毫秒。此处100秒回自动刷新缓存。
size:引用数目,代表缓存最多可以储存多少个对象。
readOnly:只读,意味着缓存数据只能读取,不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,它的默认值是false(可读/可写)。设置为true以后,查询出来的对象地址相同。
自定义缓存(原博客)
1、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、实现org.apache.ibatis.cache.Cache接口
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MybatisRedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id; // cache instance id
private RedisTemplate redisTemplate;
private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public MybatisRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
*/
@Override
@SuppressWarnings("unchecked")
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
logger.debug("Put query result to redis");
}
/**
* Get cached query result from redis
*/
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
logger.debug("Get cached query result from redis");
return opsForValue.get(key);
}
/**
* Remove cached query result from redis
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
logger.debug("Clear all the cached query result from redis");
}
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
//实现类对象不是由spring容器管理的,不能注入对象
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}
3、编写获取redisTemplate对象的工具类
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
public void setApplicationContext(ApplicationContext applicationContext) {
ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
static <T> T getBean(String name) {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder");
}
return (T) applicationContext.getBean(name);
}
}
4、配置mapper.xml文件
<?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.boot.mybatis.dao.UserMapper">
<!-- <cache-->
<!-- eviction="LRU"-->
<!-- flushInterval="100000"-->
<!-- size="512"-->
<!-- readOnly="true"-->
<!-- />-->
<cache type="com.boot.mybatis.cache.MybatisRedisCache"/>
<resultMap ...>
...
</mapper>
5、配置redis端口和地址(Redis服务的安装和管理)
spring:
redis:
host: 127.0.0.1
port: 6379
6、测试了notTheSameSqlSession(),打印结果如下:(如果再次测试,则两次都不会访问数据库)
一次查询数据库,一次查询缓存。
2020-02-12 15:54:09.709 DEBUG 4192 --- [ main] c.b.m.dao.UserMapper.selectByPrimaryKey : ==> Preparing: select id, user_name, password, create_time, update_time from user where id = ?
2020-02-12 15:54:09.731 DEBUG 4192 --- [ main] c.b.m.dao.UserMapper.selectByPrimaryKey : ==> Parameters: 1(Integer)
2020-02-12 15:54:09.746 DEBUG 4192 --- [ main] c.b.m.dao.UserMapper.selectByPrimaryKey : <== Total: 1
2020-02-12 15:54:09.755 DEBUG 4192 --- [ main] com.boot.mybatis.dao.UserMapper : Cache Hit Ratio [com.boot.mybatis.dao.UserMapper]: 0.5
7、查看Redis Desktop Manager,如下图: