Mybatis源码阅读之--本地(一级)缓存实现原理分析

前言:

Mybatis为了提升性能,内置了本地缓存(也可以称之为一级缓存),在mybatis-config.xml中可以设置localCacheScope中可以配置本地缓存的作用域,包含两个值session和statement,其中session选项表示本地缓存在整个session都有效,而statement只能在一条语句中有效(这条语句有嵌套查询--nested query/select)。

下面分析一下mybatis本地缓存的实现原理。

本地缓存是在Executor内部构建,Executor包含了四个实现类,SimpleExecutor,BatchExecutor以及CachingExecutor和RoutingExecutor,其中CachingExecutor是开启了二级缓存才会用到的,这里先不说,而RoutingExecutor是负责路由的,它本身包含了Executor,也先不管,这里主要是SimpleExecutor和BatchExecutor,他们都实现了BaseExecutor,而BaseExecutor中正是进行了一级缓存的处理。

public abstract class BaseExecutor implements Executor {
  protected PerpetualCache localCache; // 一级缓存,实质就是一个HashMap<Object, Object>
  protected PerpetualCache localOutputParameterCache; // 出参一级缓存,当statment为callable的时候使用
}

在BaseExecutor中定义了一个PerpetualCache类型的localCache属性,用来保存一级缓存

而PerpetualCache类的主要功能如下:

public class PerpetualCache implements Cache {
  private final String id; // 该缓存的id
  private final Map<Object, Object> cache = new HashMap<>();
  // ...其他一些获取缓存数据、移除缓存数据的方法
}

其中包含了两个属性,id表示缓存的唯一标识,cache是一个HashMap类型的对象,里面存放所有已经缓存的数据

也就是是说Mybatis的一级缓存实质就是一个HashMap。

再回过头看一看BaseExecutor中的一级缓存处理过程(下述中的代码片段都是BaseExecutor类中的,不会再把类加上了):

  1. select添加缓存
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 获得缓存键
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 根据cachekey执行查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

首先创建缓存键key,然后根据key再查询。

下面代码展示了根据key进行查询的逻辑

@Override
  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();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

先看第二个if--如果当前的查询语句设置了清除缓存的属性为true,那么就要把一级缓存清除

当然里面还需要满足queryStack==0的条件,这个条件涉及到了嵌套查询(nested select/query),如果是嵌套查询的最外层查询(第一个查询),才进行缓存的清理动作,否则不进行。这里的queryStack是查询的层级,取决于nested select的层数,例如一个Blog有一个Author,一个Author有一个Account,其中Author和Account都使用了嵌套查询,并且不是延迟加载(fetchType设置),那么Author查询的时候queryStack就会是1,Account查询的时候queryStack为2。针对嵌套查询这里就说这么多,后续会专门写一篇嵌套查询原理的文章,包括非延迟加载以及延迟加载的不同情况的处理方式。

    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }

下面代码片段展示了从缓存中取数据的逻辑

  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
``
直接调用localCache的getObject方法,但是需要在resultHandler不为null的情况,因为如果查询数据是传入了ResultHandler,那么会返回null,数据由ResultHandler进行处理。
如果缓存中查到了数据,那么会处理缓存的出参(出参只有在MappedStatement类型为Callable时才会有,其他的STATEMENT/PREPAREDSTATMENT都没有)
如果没有查到数据,那么从数据库中查询
```java
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

数据查询完了之后执行queryStack--操作。

进入queryFromDatabase方法进行分析:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 这里为什么要先占位呢?
    // 回答:嵌套的延迟加载有可能用的是同一个对象,这里说明已经开始查了,
    // 但是由于处理嵌套的查询,此查询还没有查完,再次执行嵌套查询,且查询的是相同的东西,那么就不用再查了
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 删除占位
      localCache.removeObject(key);
    }
    // 将数据放入到缓存中
    localCache.putObject(key, list);
    // 对于callable的statement来说,出参也需要缓存,而出参也是放在了入参中
    // 因此这里缓存了入参
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

主要步骤:

  1. 先在一级缓存中设置一个占位符,EXECUTION_PLACEHOLDER,

    此处代码的作用就是为了防止嵌套查询是查询了相同的数据

    举个例子,一个Blog有一个Author,而Author中又嵌套了一个Blog,那么Blog还没有放到缓存中,但是嵌套查询现在查Author,Author中的Blog又是第一个Blog查询的数据,这里放置一个占位符就是为了说明,这个Blog已经在查询了,结果还没出来而已,不要急,等结果出来了再进行配对。

  2. 执行子类的doQuery方法,查询数据
  3. 删除缓存占位、将查询出的数据放入到缓存中。
  4. 如果此查询语句是CALLABLE类型的,那么要把出参也缓存

    以上四部做完之后从数据库中查询数据就结束了,其中第一步可能有些人还是很困惑,大家可以执行一些测试看一看。

再次将思路返回到query方法中,

    if (queryStack == 0) { // 最外层的查询已经结束
      // 所有非延迟的嵌套查询也已经查完了,那么就可以把嵌套查询的结果放入到需要的对象中
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }

当最外层查询结束时,需要执行一些清理动作:

  1. 执行所有嵌套查询的连接操作,上面例子中的Blog->Author->Blog,会把author中的Blog设置正确
  2. 清除嵌套查询
  3. 如果当前语句的一级缓存作用域是statement的话,要把一级缓存清空

    上面的第一步和第二部需要结合ResultSetHandler共同分析,后面分析嵌套查询的时候再做详细的介绍,这里大家心中有个了解即可。

    至此,查询过程的缓存处理就已经结束了

    下面简单看一下cleanLocalCache方法

  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

也很简单,就是把localCache和localOutputParameterCache置空。

接下来就分析update(其中insert/update/delete都统称为update)时,一级缓存如何处理:

  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 先清除缓存
    clearLocalCache();
    // 使用子类的doUpdate方法
    return doUpdate(ms, parameter);
  }
``
先把缓存清空,然后调用子类的doUpdate执行具体的更新操作

另外事务的提交以及回滚都会清空以及缓存,代码如下:
```java
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache(); // 清除缓存
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache(); // 清理缓存
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache(); // 清理缓存
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

因此,在一个sqlSession执行了commit或者rollback方法后,一级缓存已经没有了数据,如果再次执行相同的查询操作,那么会重新从数据库中查询。

一级缓存需要注意的事项:

在实际开发中,有可能对查询数据进行一些操作,比如修改一些字段,或者一个列表中删除/添加一些数据,再次执行相同的查询,返回的不会是数据库中的数据,而是经过修改的数据,因此最好不要对Mybatis返回的数据进行修改操作。

原文地址:https://www.cnblogs.com/autumnlight/p/12652593.html

时间: 2024-08-02 20:52:32

Mybatis源码阅读之--本地(一级)缓存实现原理分析的相关文章

Mybatis源码阅读之--整体执行流程

Mybatis执行流程分析 Mybatis执行SQL语句可以使用两种方式: 使用SqlSession执行update/delete/insert/select操作 使用SqlSession获得对应的Mapper,然后调用mapper的相应方法执行语句 其中第二种方式获取Mapper的流程在前面已经解析过,请查看文章Mybatis源码阅读之--Mapper执行流程 其实这个方法最后的MapperMthod也是调用SqlSession的相应方法执行增删该的操作,这边文章主要介绍SqlSession执

MyBatis源码阅读

编程式开发使用MyBatis 在研究MyBatis源码之前,先来看下单独使用MyBatis来查询数据库时是怎么做的: 1 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); 2 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 3 SqlSession s

mybatis源码阅读 (五)

mybatis中的缓存,有一个疑问为什么一级缓存需要先放一个占位值,查询到结果后再移除,放入真正的值???代码标红处 1.二级缓存 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(p

mybatis源码阅读-高级用法(二)

新建学生表和学生证表 --学生表 CREATE TABLE student( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'id', `name` VARCHAR(20) NOT NULL COMMENT '姓名', `age` INT NOT NULL COMMENT '年龄', sex INT NOT NULL COMMENT '性别 1 男 0 女', cid INT NOT NULL COMMENT '班级id', cardId

mybatis源码阅读-SqlSessionFactory(三)

我们的一个mybatis程序 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); //返回的DefaultSqlSessionFactory的实例 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder .build(ClassLoader.getSystemResourceAsStream("mybatis.x

mybatis源码阅读(三)

SqlSesion怎么获取一个Mapper? 一个Mapper接口没有一个实现类怎么能够实例化? public <T> T getMapper(Class<T> type) { // 通过 configuration 的getMapper方法获取Mapper对象 return configuration.<T>getMapper(type, this); } public <T> T getMapper(Class<T> type, SqlSes

mybatis源码阅读(二)

通过SqlSessionFactory 创建 SqlSession // 通过SqlSessionFactory 获取创建一个SqlsessionSqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), nu

SpringMVC源码阅读:拦截器

1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring4.3.7)分析,弄清楚SpringMVC拦截器的工作原理 2.源码分析 进入SpringMVC核心类DispatcherServlet的doDispatch方法,在SpringMVC源码阅读:核心分发器DispatcherServlet曾经分析过,这里再分析一遍 936行获得HandlerExec

Java源码阅读

源码阅读目的是为了了解Java原理,学习优秀的类设计,整体阅读顺序和侧重主要参考基础类和常用类,参考网上整体归纳如下: 包 java.lang 1) Object 1 2) String 1 3) AbstractStringBuilder 1 4) StringBuffer 1 5) StringBuilder 1 6) Boolean 2 7) Byte 2 8) Double 2 9) Float 2 10) Integer 2 11) Long 2 12) Short 2 13) Thr