Mybatis缓存(一级缓存、二级缓存和自定义缓存)

Mybatis的缓存包括一级缓存和二级缓存,默认开启一级缓存,二级缓存需配置。一级缓存局限于SqlSession,而二级缓存在Mapper级别可共享。当执行增删改操作并提交时,二级缓存会被清除。二级缓存对象需实现Serializable接口,其默认策略是LRU,并可配置回收策略、刷新间隔和最大存储数量。自定义缓存需实现Cache接口并配置相关参数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简要说明:

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,如下图:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值