Emove

  • 首页
  • 归档
  • 分类
  • 标签

  • 搜索
context 反射 channel LRU BeanDefinition JVM 装饰者模式 MyBatis 快慢指针 归并排序 链表 hash表 栈 回溯 贪心 主从复制 二分查找 双指针 动态规划 AOF RDB 规范 BASE理论 CAP B树 RocketMQ Sentinel Ribbon Eureka 命令模式 访问者模式 迭代器模式 中介者模式 备忘录模式 解释器模式 状态模式 策略模式 职责链模式 模板方法模式 代理模式 享元模式 桥接模式 外观模式 组合模式 适配器模式 建造者模式 原型模式 工场模式 单例 UML 锁 事务 sql 索引

深入理解MyBatis-一级缓存

发表于 2020-07-29 | 分类于 MyBatis源码分析 | 0 | 阅读次数 200

深入理解MyBatis-一级缓存


序言

​ 在上一篇中我们介绍了Executor的整体设计与实现,其中涉及到的概念包括SqlSession、Executor以及StatementHandler。但是在上一篇的章节中,主角Executor的戏份并不算多,我把重点放在了讲解Executor的整体设计与实现。这会让我们误以为Executor的功能其实很简单,其实并不然,除了Executor设计理念之外,其还涉及了其他三个重要部分,分别是一二级缓存、延迟加载以及事务管理,这三者的都是算得上是围绕着Executor的重点。其次,在查询中,还有一个比较重要的功能实现,那就是嵌套子查询,而嵌套子查询的实现又与一级缓存、延迟加载及Executor后续核心组件的实现息息相关。所以,我将在接下来的两篇章节中,分析介绍MyBatis的一级缓存及二级缓存的设计与实现。之后,在介绍完StatementHandler、MyBatis映射体系等核心概念之后,再回过头来,分析MyBatis的嵌套子查询、懒加载以及延迟加载是如何实现的。相信到时候,读者不仅会对嵌套查询的理解更为深刻,也能更全面的理解MyBatis的核心组件间是如何协同合作与分工的。


1、一级缓存

1、概述

​ 在谈及MyBatis的缓存时,相信不少Java从业者会不自觉的联想到Redis。这本身并没有错,因为现阶段绝大多数企业选择Redis来作为MyBatis的外部缓存中间件。但这会使大多数Java初级工程师或者初学者,如我自己本身,一开始会误以为MyBatis的缓存就是依赖于Redis来实现的(虽然是我学艺不精导致的,但不排除有其他的同学会存在着相同的误解)。这其中存在着两个误区,一是:使用Redis作为MyBatis的外部缓存其实是MyBatis二级缓存的扩展,二是:外部缓存的实现并不是MyBatis缓存的全部,在MyBatis中存在着一级缓存及二级缓存,而一级缓存本身并不依赖于任何的其他外部软件。

​ 在介绍一级缓存的开头,我觉得有必要声明,MyBatis的一级缓存是线程级的缓存,换个大众点的说法,一级缓存是查询缓存。

​ 为什么这么说呢?还记得在上一篇中,我们提及SqlSession是一个作用域为会话级别的对象,是线程不安全的。在每次通过SqlSessionFactory获取一个SqlSession时,我们拿到的都是一个新的SqlSession对象。而每个SqlSession与Executor是一对一的对象,Executor也是线程不安全的。一级缓存的实现逻辑又都在Executor中,所以说,一级缓存是一个线程的缓存。又因为在MyBatis中,只有查询时,才会使用到一级缓存,所以又称一级缓存为查询缓存。

​ 也就是说,一级缓存只在一次会话查询中有效。只有在一次会话查询中,我们执行了多次条件及Sql完全相同的情况下,一级缓存才能发挥它的作用,以提高查询性能。其大概的执行过程如下图所示。

一级缓存基本流程图

2、一级缓存的命中条件与场景

​ 在MyBatis中,一级缓存时默认启用的,虽然可以通过将localCacheScop的级别配置为statement来关闭一级缓存,但是这样子做并不能完全使一级缓存失效。为了验证我说的话,我们再来看看上个章节中的BaseExecutor的源码。注意,我在下方代码中删除了所有与一级缓存无关的代码。

public abstract class BaseExecutor implements Executor {
  //本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询(一级缓存)
  protected PerpetualCache localCache;
  protected Configuration configuration;
  //查询堆栈
  protected int queryStack = 0;

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    // ...
    // 每当执行update操作时,清除本地缓存
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //得到绑定sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    //创建缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {utorException("Executor was closed.");
    }
    // 当子查询栈为0时,且配置了flushCache为true时,清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      //加一,这样递归调用到上面的时候就不会再清局部缓存了
      queryStack++;
      //先根据cachekey从localCache去查
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //若查到localCache缓存,处理localOutputParameterCache
        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();
      }
      //清空延迟加载队列
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    	// 当localCacheScop级别配置为Statement时,每次执行完查询后,都会清除本地缓存。
        clearLocalCache();
      }
    }
    return list;
  }

  @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    return localCache.getObject(key) != null;
  }

  @Override
  public void commit(boolean required) throws SQLException {
    // ...
    clearLocalCache();
    // ...  
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        // 回滚时,清除一级缓存
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

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

  //从数据库查
  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);
    return list;
  }
}

​ 在大概的浏览完上面带有注释的代码后,看着下方几个要点,再对着代码过一遍。

  1. 在BaseExecutor中,维护了一个PerpetualCache来缓存查询结果。PerpetualCache属于MyBatis中Cache体系里,最终实现结果存储的实现。这个在下一章节中会讲。在PerpetualCache类中,维护了一个HashMap来存储查询数据。
  2. 在执行最终query方法之前(参数最多的query方法),会调用createCacheKey方法,来构建一个CacheKey对象,用该对象来作为一次一级缓存的唯一key。createCacheKey在上方源代码中已经被我去除,留在下面详细介绍。
  3. 加倍注意所有调用了clearLocalCache的地方,其中包括update一处、query两处、commit以及rollback各一处,也就是说,每当执行update、commit和rollback操作时,均会无条件清空一级存储。而在query方法中,清除本地缓存是有条件的。第一处是MappedStatement对象配置了flushCache等于true时,才会清空一级缓存,第二处的条件是localCacheScop级别配置为statement时。此外,这两个条件还有一个共同的条件就是需要嵌套子查询栈长度为0时才会执行。至于为什么这么做,我们留着以后再讲。

​ 接下来,我们来分析createCacheKey,看看一级缓存的唯一键与哪些条件相关。

//创建缓存Key
  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
    cacheKey.update(ms.getId());
    cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
    cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    //模仿DefaultParameterHandler的逻辑,不再重复,请参考DefaultParameterHandler
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 逐个参数值加入cacheKey
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }    

​ 可以看到,CacheKey与MappedStatement、参数、RowBounds、Sql和Environment相关。而在CacheKey的doUpdate源码中,主要是通过这些相关条件参数来生成hashCode,也就是说,只要其中任何一个条件不满足,都会导致无法命中一级缓存。

总结

​ 对于以上提及的一级缓存命中条件,大致可以分为以下两类:

  1. 运行时参数相关

    1. 同一个会话
    2. Sql语句相同
    3. 参数相同
    4. 相同的MappedStatement,即StatementID得相同
    5. RowBounds(返回行范围)得相同
    6. Environment(运行时环境)得相同】
  2. 操作与配置相关

    1. 未手动清空缓存(事务提交与回滚)
    2. 未配置flushCache等于true
    3. 未执行update操作
    4. 缓存作用域不是Statement

    下图时关于与一级缓存相关的执行流程

    image

# context # 反射 # channel # LRU # BeanDefinition # JVM # 装饰者模式 # MyBatis # 快慢指针 # 归并排序 # 链表 # hash表 # 栈 # 回溯 # 贪心 # 主从复制 # 二分查找 # 双指针 # 动态规划 # AOF # RDB # 规范 # BASE理论 # CAP # B树 # RocketMQ # Sentinel # Ribbon # Eureka # 命令模式 # 访问者模式 # 迭代器模式 # 中介者模式 # 备忘录模式 # 解释器模式 # 状态模式 # 策略模式 # 职责链模式 # 模板方法模式 # 代理模式 # 享元模式 # 桥接模式 # 外观模式 # 组合模式 # 适配器模式 # 建造者模式 # 原型模式 # 工场模式 # 单例 # UML # 锁 # 事务 # sql # 索引
深入理解MyBatis-Executor执行体系
深入理解MyBatis-二级缓存
  • 文章目录
  •   |  
  • 概览
林亦庭

林亦庭

less can be more

87 文章
11 分类
54 标签
RSS
Github
Creative Commons
© 2021 林亦庭
粤ICP备20029050号