前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >​mybatis的缓存机制源码分析之一级缓存解析

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

作者头像
用户7634691
发布2021-10-19 14:05:02
3030
发布2021-10-19 14:05:02
举报

引言

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

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

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

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

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

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

正文

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

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
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的引用,这是个本地缓存的实现类,缓存就是通过这个类来管理的。

代码语言:javascript
复制
public abstract class BaseExecutor implements Executor {

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

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

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

代码语言:javascript
复制
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场景。至于选择哪个,是配置决定的:

代码语言:javascript
复制
  <setting name="defaultExecutorType" value="SIMPLE"/>

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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持有它的引用对缓存进行管理。查询的逻辑如下:

代码语言:javascript
复制
@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:

代码语言:javascript
复制
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;
  }
代码语言:javascript
复制
public class PerpetualCache implements Cache {

  private final String id;

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

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

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

代码语言:javascript
复制
public class LruCache implements Cache {

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

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

代码语言:javascript
复制
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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
    • 正文
    相关产品与服务
    数据库
    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档