[TOC]

前言

和大多数持久化框架一样,MyBatis 提供了一级缓存和二级缓存的支持。默认情况下,MyBatis 只开启一级缓存。

MyBatis提供了一级缓存和二级缓存

  • 一级缓存:也称为本地缓存,用于保存用户在一次会话过程中查询的结果,用户一次会话中只能使用一个sqlSession,一级缓存是自动开启的,不允许关闭。
  • 二级缓存:也称为全局缓存,是mapper级别的缓存,是针对一个表的查结果的存储,可以共享给所有针对这张表的查询的用户。也就是说对于mapper级别的缓存不同的sqlsession是可以共享的。

一级缓存

一级缓存是基于 PerpetualCache(MyBatis自带)的 HashMap 本地缓存,作用范围为 session 域内,它存储的SqlSession中的BaseExecutor之中。当 session flush(刷新)或者 close(关闭)之后,该 session 中所有的 cache(缓存)就会被清空。

在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用同一个 mapper 的方法,往往只执行一次 SQL。因为使用 SqlSession 第一次查询后,MyBatis 会将其放在缓存中,再次查询时,如果没有刷新,并且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

由于 SqlSession 是相互隔离的,所以如果你使用不同的 SqlSession 对象,即使调用相同的 Mapper、参数和方法,MyBatis 还是会再次发送 SQL 到数据库执行,返回结果。

示例:

package net.biancheng.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;

import net.biancheng.po.Website;

public class Test {
    public static Logger logger = Logger.getLogger(Test.class);
    public static void main(String[] args) throws IOException {

        InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根据配置文件构建
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
        SqlSession ss = ssf.openSession();

        Website site = ss.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
        logger.debug("使用同一个sqlsession再执行一次");
        Website site2 = ss.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
        // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
        ss.commit();

        logger.debug("现在创建一个新的SqlSeesion对象在执行一次");
        SqlSession ss2 = ssf.openSession();
        Website site3 = ss2.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
        // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
        ss2.commit();
    }
}

结果如下:

DEBUG [main] - ==>  Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
DEBUG [main] - 使用同一个sqlsession再执行一次
DEBUG [main] - 现在创建一个新的SqlSeesion对象在执行一次
DEBUG [main] - ==>  Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1

从运行结果可以看出,第一个 SqlSession 实际只发生过一次查询,而第二次查询就从缓存中取出了,也就是 SqlSession 层面的一级缓存。为了克服这个问题,我们往往需要配置二级缓存,使得缓存在 SqlSessionFactory 层面上能够提供给各个 SqlSession 对象共享。

一级缓存失效的四种情况

  1. sqlsession变了 缓存失效
  2. sqlsession不变,查询条件不同,一级缓存失效
  3. sqlsession不变,中间发生了增删改操作,一级缓存失败
  4. sqlsession不变,手动清除缓存,一级缓存失败

spring结合mybatis后,一级缓存作用:

  • 在未开启事务的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是不起作用的

  • 在开启事务的情况之下,spring使用ThreadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的。

    由于spring使用了代理, 所以在代理结束时就关闭了sqlsession, 导致一级缓存失效

Spring整合MyBatis时一级缓存失效问题_xingze_W的博客-CSDN博客_spring整合mybatis一级缓存

mybatis一级缓存失效的四种情况 - 简书 (jianshu.com)

禁用一级缓存

mybatis:
  configuration:
    cache-enabled: false  #禁用二级缓存
    local-cache-scope: statement  #默认指定为session级别

指定缓存statement级别后, mybatis会删除缓存

org.apache.ibatis.executor.BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) throw new ExecutorException("Executor was closed.");
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
        // 此处做了删除
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache(); // issue #482
      }
    }
    return list;
  }

二级缓存

二级缓存是全局缓存,作用域超出 session 范围之外,可以被所有 SqlSession 共享。二级缓存的作用域是namespace,也就是作用范围是同一个命名空间

开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

img

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。 当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

禁用二级缓存

mybatis:
  configuration:
    cache-enabled: false #禁用二级缓存, 默认为true

mybatis的二级缓存是本地实现的, 在分布式环境中, 容易产生脏数据 , 所以一般会使用redis这种第三方存储来使用二级缓存

SpringBoot+Mybatis一级缓存和二级缓存详解 - 郑晓龙 - 博客园 (cnblogs.com)

mybatis 二级缓存失效_给我五分钟,带你彻底掌握MyBatis的缓存工作原理_Tfifthe的博客-CSDN博客

MyBatis缓存(一级缓存和二级缓存) (biancheng.net)

MyBatis|缓存机制 - 简书 (jianshu.com)

springboot+mybatis一级缓存启用/禁用问题_NongYeting的博客-CSDN博客_springboot关闭mybatis缓存