BaseExecutor 与一级缓存

前面提到的三个基础执行器都是需要和数据库进行直接交互的。

mybatis 本身有两层缓存结构。部分情况下的查询操作,可能并不会请求数据库,而是通过框架提供的两级缓存就完成了处理并返回。

一 BaseExecutor

基础执行器,这一层级包含了 mybatis 两级缓存中的第一级缓存。

mybatis 默认开启第一级缓存。

其次,执行器的设计分层遵循了软件设计的 单一职责 原则。BaseExecutor 只管理一级缓存,而具体的数据库交互逻辑,是交由更低层的三个执行器处理的(Simple/Reuse/Batch)。

1.1 构造函数

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    // 事物对象
    this.transaction = transaction;
    // 延迟加载队列
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    // 一级缓存
    this.localCache = new PerpetualCache("LocalCache");
    // 本地输出参数缓存
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    // 执行器状态标识对象
    this.closed = false;
    // mybatis 配置对象
    this.configuration = configuration;
    this.wrapper = this;
  }

一级缓存就是构造中的 localCache,可以看到是 mybatis 自定义对象

org.apache.ibatis.cache.impl.PerpetualCache

点开看一下,可以知道其实还是对 hashmap 的一个包装。除此之外新增的类成员是 id。类中复写了 equals 方法,看到这部分代码,即可知道这个 id 字段的作用,是用来标识全局范围内的唯一性。两个不同的 PerpetualCache 对象的检查,就是依赖 id 字段识别。构造函数中创建了两个 PerpetualCache,构造入参就是用来识别的 id。

1.2 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();
    // 执行更新操作
    return doUpdate(ms, parameter);
  }

一级缓存的作用域是 Session 级别,即会话层级,当 session 域内执行更新操作的时候,会直接清空一级缓存,没有做比较细的区分处理。比如 update 操作更新的是表 b,而一级缓存保有了表 a 的查询结果,但是此时更新表 b,一级缓存中关于表 a 的内容也会被清空。

  • 坏处是这样导致一级缓存的保存时间短,命中率降低
  • 好处是该级缓存管理难度低,刷新率高也会让缓存的空间占用保持在一个较低的水平
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

清除缓存操作,处理了两个类成员

  • localCache
  • localOutputParameterCache

这俩就是一级缓存的组成。

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

下面执行的更新操作,是在抽象类中定义的抽象方法,具体实现,则落到了前面提到过的三个基础执行器上面。这里就是 单一职责 原则的体现。BaseExecutor 只负责处理一级缓存,具体和数据库交互的逻辑,被交由基础执行器。

1.3 query 操作

  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.");
    }
    // 如果查询堆栈为0(即没有递归调用),清除缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    // 执行查询逻辑
    try {
      // 查询堆栈记录值 +1
      queryStack++;
      // 根据 cacheKey 检查是否命中缓存
      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) {
      // 查询堆栈为0,检查延迟加载堆栈中的数据并加载
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // 清空延迟加载数据
      // issue #601
      deferredLoads.clear();
      // 如果一级缓存的作用域被设置到 stmt 级别,就在执行完查询后清空一级缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  }

session 级别调用到 query 是这个方法。如果只是简单基础的查询逻辑的话,这个方法本不会这么复杂。但是涉及到了存储过程/包含嵌套查询的复杂 sql 等场景。所以衍生出来了较多的分支逻辑。

如下的字符串,是查询方法中,检索一级缓存是否命中时传入的关键参数 cachekey。这个 cachekey 的组成并不是很简单的 namespace+id 拼凑而成。而是杂糅了诸多因素进去。这些组成因子也因此可以决定一级缓存是否可以命中。

1298872468:4918445298:com.gaop.mapper.WaterMapper.getById:0:2147483647:select * from water_00 where id = ?:1:development

1.4 缓存 key 的组成与一级缓存的命中条件

在方法

PerpetualCache.get(Object key)

中,实际是执行了 map.get() 方法。所以检索 key 是否匹配,检查的是传入对象的 equals 方法。而 mybatis 传入的 key,固定了类型为 CacheKey。

创建一个 cacheKey 的流程需要聚焦到另一个方法

org.apache.ibatis.executor.BaseExecutor#createCacheKey

这个方法是用于创建以及填充一个完整的 cacheKey 对象的执行流程。

  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 初始化缓存 key 对象,这个对象有三个默认参数值 hashcode-17,multiplier-37,count-0
    CacheKey cacheKey = new CacheKey();
    // 依次加入 namespaceid/mybatis分页参数/绑定的sql,用于生成哈希码
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    // 最后处理sql入参内容,并将处理后的入参加入 hashcode 生成
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      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.update(value);
      }
    }
    // 这里一个补充因子,mybatis 环境配置,不同环境的 sql 不能混在一起
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

整个创建逻辑的核心内容在于 hashcode 的生成。 每一个加入 hashcode 生成流程的参数都会影响到一级缓存是否命中。 我们可以总结一下有那些因子:

  1. namespaceid
  2. mybatis 分页参数 limite/offset
  3. sql
  4. sql 参数
  5. myabtis 环境参数

创建完一个 cacheKey 之后,需要看一下比较一个 cacheKey 的核心逻辑。因为一级缓存用的就是用这个 cacheKey 作为 hashmap 的 key 值。

如下的内容为

org.apache.ibatis.cache.CacheKey#equals

的方法内容,也就是一级缓存是否命中的检查逻辑执行的核心内容。

  public boolean equals(Object object) {
    // 如果比较对象是自己,通过
    if (this == object) {
      return true;
    }
    // 如果传入的 key 的类型不是 cacheKey,则不通过
    if (!(object instanceof CacheKey)) {
      return false;
    }
    // 对象类型转换
    final CacheKey cacheKey = (CacheKey) object;
    // 比较类成员哈希码值是否匹配,不匹配则不通过
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    // 交验参数和是否匹配,不匹配则不通过
    if (checksum != cacheKey.checksum) {
      return false;
    }
    // 如果是更新类型操作,会传入入参参数总数,这个值不匹配也不会通过
    if (count != cacheKey.count) {
      return false;
    }
    // updateList 是按顺序加入生成 hashcode 的参数列表,hashcode 相等不意味着传入的参数完全一致,所以这里需要比较生存 hashcode 参数的列表。如果出现不一致的,则不通过。
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

其实一段 hashcode 的代码检查逻辑,整体思路就和 hashmap 里检索 key 的思路很相似了。先检查 hashcode 是否相等,再检查组成 hashcode 的数据是否相等。

二 简要总结

BaseExecutor 处于 mybatis 执行器体系的中间层。其设计与实现遵循了 单一职责 原则。具体体现在 baseExecutor 主要聚焦在处理一级缓存的逻辑上,而与数据库交互的具体实现依赖于另外三个底层执行器(simple/reuse/batch)。

一级缓存默认是开启的,作用域默认会话层级(session)。任意更新操作都会清空一级缓存中的所有数据。一级缓存有一个 mybatis 的自定义实现类

org.apache.ibatis.cache.impl.PerpetualCache

但是其内容也是对 hashmap 的一个包装。所以核心数据结构还是 hashmap

该缓存 key 的生成逻辑依赖了一些列的参数,这些参数也就是影响一级缓存命中的因素列表。有一个专门的类用于保存这些数据

org.apache.ibatis.cache.CacheKey

影响一级缓存命中的因素有

  • namespaceid
  • mybatis 分页参数 limit/offset
  • sql
  • sql 入参
  • mybatis 环境配置参数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343