mybatis的缓存机制源码分析之一级缓存解析

Posted 犀牛饲养员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mybatis的缓存机制源码分析之一级缓存解析相关的知识,希望对你有一定的参考价值。

文章目录

引言

本篇源码解析基于mybatis 3.5.8版本。

MyBatis 中的缓存指的是 MyBatis 在执行一次SQL查询时,在满足一定的条件下,会把这个sql和对应的查询结果缓存起来。当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是请求到数据库。当然如果中间有更新操作,缓存会失效。

MyBatis中的缓存分为一级缓存和二级缓存,一级缓存又被称为 SqlSession 级别的缓存,二级缓存又被称为表级缓存。通俗的说,一级缓存是本次会话有效,二级缓存可以跨越多个会话共享缓存。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

一级缓存开启的情况下,查询的时序图如下:

二级缓存也是类似的机制。

正文

根据上面的调用时序图我们可以看到mybatis是通过SqlSession统一对外的,SqlSession是接口,有个默认实现类:DefaultSqlSession。来看下这个类。

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  //每个SqlSession中持有了Executor
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;
  ...

DefaultSqlSession持有Executor的引用,事实上对sqlSession的操作都是委托给这个Executor进行的,比如select方法,最终会调用selectList方法:

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

Executor是个接口,有个默认的抽象类实现BaseExecutor,这个抽象类持有一个名为``PerpetualCache`的引用,这是个本地缓存的实现类,缓存就是通过这个类来管理的。

public abstract class BaseExecutor implements Executor {

  ...
  //本地缓存实现类
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;
  ...

到这里,我们总结下提到的这几个类的关系:

前面提到BaseExecutor是个抽象类,定义若干抽象方法,方法如下:

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;

在执行的时候,把具体的操作委托给对应的实现类进行执行。有几个实现类,为Executor赋予了不同的能力,如下图所示:

BatchExecutor专门用于执行批量sql操作。而SimpleExecutor用于简单sql场景。至于选择哪个,是配置决定的:

  <setting name="defaultExecutorType" value="SIMPLE"/>

既然DefaultSqlSession持有Executor的引用,它是什么时候初始化的呢?答案是在org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession调用的时候,如下:

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      ....
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

继续跟下去,org.apache.ibatis.session.Configuration#newExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    //默认的executor类型是SIMPLE
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    /**
     * 插件化处理
     */
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

大部分时候,我们使用的是SimpleExecutor,注意二级缓存如果开关打开使用的是CachingExecutor,我们下篇文章再来分析二级缓存。 插件化处理那里,后面也会有专门的文章进行解析。

来继续看看PerpetualCache,BaseExecutor持有它的引用对缓存进行管理。查询的逻辑如下:

@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--;
    }
    ...

代码逻辑比较清晰,先查缓冲,没有命中就查数据库。PerpetualCache其实是持有一个map进行本地缓存。然后查询db后再更新cache:

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);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();
  ...

Cache接口有几个实现类,结构如下:

这些不同的实现类彼此通过装饰器模式互相装饰,实现功能的互补。比如说,

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;
...

初始化的时候可以传入一个其它cache实现类的,复用这个类的功能。

LruCache cache = new LruCache(new PerpetualCache("default"));
    cache.setSize(5);
    for (int i = 0; i < 5; i++) {
      cache.putObject(i, i);
    }
    ...

具体的每个实现类不是本文的重点,这里不表。

一级缓存就写到这里吧。


参考

  • https://tech.meituan.com/2018/01/19/mybatis-cache.html

以上是关于mybatis的缓存机制源码分析之一级缓存解析的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis源码解析MyBatis的二级缓存源码解析

Mybaits 源码解析 ----- 全网最详细,没有之一:一级缓存和二级缓存源码分析

#yyds干货盘点# mybatis源码解读:cache包(缓存机制功能)

Mybatis 缓存 源码分析

聊聊MyBatis缓存机制

MyBatis 源码分析 - 缓存原理