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

autumnlight      2022-05-07     298

关键词:

前言:
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返回的数据进行修改操作。

mybatis源码阅读

mybatis中的缓存,有一个疑问为什么一级缓存需要先放一个占位值,查询到结果后再移除,放入真正的值???代码标红处 1、二级缓存public<E>List<E>query(MappedStatementms,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandle... 查看详情

mybatis源码分析五mybatis的缓存

五、MyBatis缓存文章目录五、MyBatis缓存缓存的概念与应用缓存的概念开发一个简单的缓存MyBatis中的缓存设计自定义一个Cache实现类MyBatis中的Cache实现类PerpetualCache装饰器CacheCache如何在MyBatis运行过程应用MyBatis缓存的二层体系一级... 查看详情

mybatis源码分析五mybatis的缓存

五、MyBatis缓存文章目录五、MyBatis缓存缓存的概念与应用缓存的概念开发一个简单的缓存MyBatis中的缓存设计自定义一个Cache实现类MyBatis中的Cache实现类PerpetualCache装饰器CacheCache如何在MyBatis运行过程应用MyBatis缓存的二层体系一级... 查看详情

通过源码分析mybatis的缓存

  看了通过源码分析MyBatis的缓存这篇文章后,自己跟着源码过了一遍,对mybatis的一级缓存和二级缓存有了更清楚的认识。  一级缓存是SqlSession级别的,同一个sqlSession在第二次执行一个相同参数的select语句并且第一次执行... 查看详情

mybatismybatis之缓存(代码片段)

MyBatis缓存介绍Mybatis使用到了两种缓存:一级缓存(本地缓存、localcache)和二级缓存(secondlevelcache)。  一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为 Session,当 Sessionflush 或 close 之后,该Se... 查看详情

mybatis之缓存(代码片段)

目录一、简介     PerpetualCache增强的缓存功能分类二、原理1、PerpetualCache源码2、LRUCache,装饰器增强的缓存3、CacheKey4、一级缓存、二级缓存三、一级缓存访问&创建删除四、二级缓存开启命名空间划分访问&更新删除... 查看详情

《深入理解mybatis原理6》mybatis的一级缓存实现详解及使用注意事项

《深入理解mybatis原理》MyBatis的一级缓存实现详解及使用注意事项0.写在前面 MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上。MyBatis提供了一级缓存、二级缓存这两个缓存机制,... 查看详情

mybatis组件之缓存实现及使用

 一.概述先讲缓存实现,主要是mybatis一级缓存,二级缓存及缓存使用后续补充Mybatis缓存的实现是基于Map的,从缓存里面读写数据是缓存模块的核心基础功能;除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添... 查看详情

mybatis的缓存机制源码分析之二级缓存解析(代码片段)

引言本篇源码解析基于mybatis3.5.8版本。MyBatis中的缓存指的是MyBatis在执行一次SQL查询时,在满足一定的条件下,会把这个sql和对应的查询结果缓存起来。当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,... 查看详情

mybatis-缓存机制

MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率。MyBatis系统中默认定义了两级缓存,一级缓存和二级缓存。–1、默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本... 查看详情

mybatis缓存专题-一文彻底搞懂mybatis一级缓存(代码片段)

...不能使用2.什么是一级缓存3.什么情况下会命中一级缓存4.Mybatis的一级缓存机制详解5.MyBatis关闭一级缓存6.Mybatis的一级缓存机制源码分析7.Mybatis的一级缓存机制源码分析图解总结8.一级缓存什么时候被清空?9.一级缓存key是什... 查看详情

mybatis缓存专题-一文彻底搞懂mybatis一级缓存(代码片段)

...不能使用2.什么是一级缓存3.什么情况下会命中一级缓存4.Mybatis的一级缓存机制详解5.MyBatis关闭一级缓存6.Mybatis的一级缓存机制源码分析7.Mybatis的一级缓存机制源码分析图解总结8.一级缓存什么时候被清空?9.一级缓存key是什... 查看详情

源码级mybatis缓存策略(一级和二级缓存)(代码片段)

...们可以避免频繁的与数据库进行交互,进而提高响应速度MyBatis也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:①、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一... 查看详情

mybatis缓存的使用和源码分析

Mybatis缓存使用在Mybatis中缓存分为一级缓存和二级缓存,二级缓存又称为全局缓存,默认一级缓存和二级缓存都是开启的,只是二级缓存的使用需要配置才能生效,在Mybatis中一级缓存是SqlSession级别也就是会话级别的,而二级缓... 查看详情

mybatis缓存

MyBatis缓存MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存默认情况下,只有一级缓存开启。(SqlSession级别的... 查看详情

mybatis缓存之二级缓存

二级缓存(全局缓存):基于namespace级别的缓存,一个namespace对应一个二级缓存。工作机制:一个会话,查询一条数据,这条数据会放在当前会话的一级缓存中;如果会话关闭,该会话对应的一级缓存就消失了;可以使用二级缓... 查看详情

mybatis缓存机制

MyBatis提供了一级缓存和二级缓存的支持。一级缓存一级缓存是基于PerpetualCache的HashMap本地缓存;一级缓存的作用域是SqlSession,即不同的SqlSession使用不同的缓存空间;一级缓存的开启和关闭一级缓存是默认开启的;关闭一级缓存... 查看详情

mybatis一级缓存案例演示&源码级原理探究~~~(代码片段)

文章目录简介什么是Mybatis一级缓存?案例演示基于同一个SqlSession实验基于不同SqlSession实验小结论源码探究回顾标题:Mybatis一级缓存是什么???最后总结为什么一级缓存又叫查询缓存?Java入门到就业学... 查看详情