Mybatis源码解析(六):一级缓存和二级缓存的优先级

news2025/4/11 1:02:07

Mybatis源码系列文章

手写源码(了解源码整体流程及重要组件)

Mybatis源码解析(一):环境搭建

Mybatis源码解析(二):全局配置文件的解析

Mybatis源码解析(三):映射配置文件的解析

Mybatis源码解析(四):sql语句及#{}、${}的解析

Mybatis源码解析(五):SqlSession会话的创建

Mybatis源码解析(六):一级缓存和二级缓存的优先级


目录

  • 前言
  • 一、会话对象selectOne方法
  • 二、获取缓存对象key
    • 1、获取BoundSql对象
    • 2、获取缓存的key值
      • 1)CacheKey构造方法
      • 2)更新方法
  • 三、二级缓存的流程
  • 四、一级缓存的流程
  • 总结


前言

  • 之前篇章讲了配置文件的解析与SqlSession的创建,可以说都是在为执行增删改查操作主流程做铺垫
  • 接下来让我们进入SqlSession的selectOne实现方法

在这里插入图片描述


一、会话对象selectOne方法

selectOne方法

  • SqlSession接口有两个实现类:
    • DefaultSqlSessionFactor(通过上一个步骤创建–>Mybatis源码解析(五):SqlSession会话的创建)
    • SqlSessionManager(已弃用)
  • 方法入参:
    • statement:statementId = “namespace.id”
    • parameter:方法参数,填充带?的sql
  • this.selectList:调用同类的方法,参数传递,从这里可以看出,selectOne方法的实现都交给了selectList,获取到只取第一个值
    • 只有一个值,获取返回即可
    • 如果是空,则返回空
    • 如果大于一个,则报错,因为此方法目的是查询唯一的一个值,结果多个
@Override
public <T> T selectOne(String statement, Object parameter) {
  // Popular vote was to return null on 0 results and throw exception on too many.
  List<T> list = this.selectList(statement, parameter);
  if (list.size() == 1) {
    return list.get(0);
  } else if (list.size() > 1) {
    throw new TooManyResultsException("Expected one result (or null) 
    to be returned by selectOne(), but found: " + list.size());
  } else {
    return null;
  }
}

selectList方法

  • 三个selectList方法依次向下调用
  • 第一个方法添加默认分页对象调用第二个方法
  • 第二个方法添加结果集处理器空对象调用第三个方法
  • 第三个核心方法:
    • 通过statementId获取MappedStatement(由每个<select><insert><update><delete>内属性构建而成的对象)
    • executor:默认无配置情况下,是简单执行器外面包一层缓存执行器。具体这里又讲:Mybatis源码解析(五):SqlSession会话的创建
// 第一个
@Override
public <E> List<E> selectList(String statement, Object parameter) {
  // 调用重载方法
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

// 第二个
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  // 参数4:结果集处理器
  return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
}

// 第三个
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // 根据传入的statementId,获取MappedStatement对象
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 调用执行器的查询方法
    // wrapCollection(parameter)是用来装饰集合或者数组参数
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

二、获取缓存对象key

1. 从MappedStatement对象中获取BoundSql对象

2. 获取一级或二级缓存Map的key,value是查询的结果

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取绑定的SQL语句,比如 "SELECT * FROM user WHERE id = ? "
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 生成缓存Key
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

1、获取BoundSql对象

  • 通过MappedStatement对象中的sqlSource属性对象(Mybatis源码解析(四):sql语句及#{}、${}的解析-讲解了如何构建SqlSource)
  • getBoundSql方法:动态sql或${}则动态构建,#{}则获取以前准备好的数据构建对象(Mybatis源码解析(四)有讲)
public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  //  
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

进入getBoundSql

  • 这里只展示非动态sql(#{}样式),动态的sql需要动态组装sql,而非动态带?的sql已经准备好了
  • BoundSql的组成:
    • 带?的sql
    • #{}或${}中的属性参数
    • 入参属性值
@Override
public BoundSql getBoundSql(Object parameterObject) {
  return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}

2、获取缓存的key值

进入CachingExecutor的createCacheKey方法

  • delegate:简单执行器、批量执行器、重用执行器其中的一种,这里使用他们的父类Executor接受
  • Mybatis源码解析(五):SqlSession会话的创建-默认情况下,缓存开启,则以上的三种执行器会在外面包一层(创建一个缓存执行器,构造函数传入以上执行器,jdbc操作由他们处理,缓存操作由缓存执行器处理)
  • 由于创建key是不同执行器的公共操作,则方法在他们三的抽象父类BaseExecutor中
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

进入BaseExecutor的createCacheKey方法

  1. 创建CachKey对象(一二级缓存都用这个当key)
  2. update方法更新
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 创建cacheKey对象
  CacheKey cacheKey = new CacheKey();
  // id
  cacheKey.update(ms.getId());
  // 分页参数
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  // sql
  cacheKey.update(boundSql.getSql());
  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);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    // 当前环境的值也会设置
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}

1)CacheKey构造方法

  • 初始化一些字段值,为之后重写hashCode和equals方法做准备
...
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;

// 参与hash运算的乘数
private final int multiplier; 
// cachekey的hash值,在update函数中实时算出来
private int hashcode; 
// 校验和,hash值的和
private long checksum; 
// updateList的中的元素个数
private int count; 
// 根据该集合中的元素判断两个cacheKey是否相同
private List<Object> updateList;

public CacheKey() {
  this.hashcode = DEFAULT_HASHCODE;
  this.multiplier = DEFAULT_MULTIPLIER;
  this.count = 0;
  this.updateList = new ArrayList<>();
}
...

2)更新方法

  • 通过入参计算hashcode值,将入参添加到更新集合
  • 入参:statementId、分页参数、带?的sql、参数值、环境id
  • 比较CacheKey对象就是比较上述参数的hashcode值,相同则返回以前存储的value,不相同则查询数据库
public void update(Object object) {
  // 获取参数的object的hash值
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

  // 更新count 、checksum以及hashcode的值
  count++;
  checksum += baseHashCode;
  baseHashCode *= count;
  hashcode = multiplier * hashcode + baseHashCode;

  // 将对象添加到list集合中
  updateList.add(object);
}

equals和hashCode方法

@Override
public boolean equals(Object object) {
  if (this == object) { // 比较是不是同一个对象
    return true;
  }
  if (!(object instanceof CacheKey)) { // 是否类型相同
    return false;
  }

  final CacheKey cacheKey = (CacheKey) object;

  if (hashcode != cacheKey.hashcode) { // hashcode是否相同
    return false;
  }
  if (checksum != cacheKey.checksum) { // checksum是否相同
    return false;
  }
  if (count != cacheKey.count) { // count是否相同
    return false;
  }

  // 按顺序比较updateList中元素的hash值是否相同
  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;
}

@Override
public int hashCode() {
  return hashcode;
}

三、二级缓存的流程

  • 回到第二章节的开头;获取完缓存的CachKey对象后,进入缓存执行器重载方法query
  • ms.getCache():namespace启动二级标签;MappedStatement对象中属性,所以在映射配置文件中配置,如下
    在这里插入图片描述
  • flushCacheIfRequired:<select>标签中的flushCache属性,刷新二级缓存,默认false
  • ms.isUseCache:<select>标签中的useCache属性,此标签启用二级缓存,默认true
  • 先从二级缓存tcm中获取,没有则委托BaseExecutor去查询一级缓存或数据库
  • 再讲结果放入二级缓存tcm,其实只是放入一个map(非二级缓存的map),因为只有提交事务才会真正添加到二级缓存(二级缓存的get和put后面篇章单独讲)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 获取二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 刷新二级缓存 (存在缓存且flushCache为true时)
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // 从二级缓存中查询数据
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 如果二级缓存中没有查询到数据,则查询一级缓存及数据库
      if (list == null) {
        // 委托给BaseExecutor执行
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 将查询结果 要存到二级缓存中(注意:此处只是存到map集合中,没有真正存到二级缓存中)
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托给BaseExecutor执行
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

四、一级缓存的流程

  • 第三章的delegate.query方法,delegate此时为简单执行器SimpleExecutor,而query方法在他的父类BaseExecutor
    在这里插入图片描述
  • queryStack为0:表示是当前会话只有本次查询而没有其他的查询了
  • ms.isFlushCacheRequired():与二级缓存中的<select>标签中的flushCache属性功能一样
  • 先从一级缓存localCache中获取,没有则从数据库查询
@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.");
  }
  // 1. 如果配置了flushCacheRequired为true,则会在执行器执行之前就清空本地一级缓存
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    // 1.1. 清空缓存
    clearLocalCache();
  }
  List<E> list;
  try {
    // 2. 查询堆栈 + 1
    queryStack++;
    // 从一级缓存中获取数据
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      // 3.1. 已有缓存结果,则处理本地缓存结果输出参数(存储过程)
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 3.2. 没有缓存结果,则从数据库查询结果
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    // 查询堆栈数 -1
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

进入queryFromDatabase方法

  • 因为延迟加载的原因,这里一级缓存先添加一个占位符
  • 查询结果以后,移除然后再put
  • 具体的查询数据库操作doQuery方法,下一篇文章专门来讲
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // 1. 首先向本地缓存中存入一个ExecutionPlaceholder的枚举类占位value
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    // 2. 执行doQuery方法
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    // 3. 执行完成移除这个key
    localCache.removeObject(key);
  }
  // 4. 查询结果存入缓存中
  localCache.putObject(key, list);
  // 5. 如果MappedStatement的类型为CALLABLE,则向localOutputParameterCache缓存中存入value为parameter的缓存
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

总结

  • 一级缓存和二级缓存就是一个Map集合对象:
    • key:Cache对象(statementId、分页参数、带?的sql、参数值、环境id)
    • value:数据库查询结果
  • 一级缓存默认开启,二级缓存需要添加 <cache>标签开启
  • 都开启情况下:先从二级缓存获取,没有则从一级缓存获取,还没有则查询数据库(查询结果后先添加到一级缓存,再添加到二级缓存)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/19508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity学习笔记[一] RollBall小游戏

目录 一、适配vs 二、初识Unity 2.1 unity核心模块 2.2 Unity基本操作和场景操作 2.3 世界坐标系和局部坐标系 2.4 工具栏 QWER 三、基础知识 3.1 基本组件 3.2 刚体组件 3.2.1 获取刚体组件 3.2.2 给刚体施加力 3.3 三维向量Vector3 3.4 通过按键控制左右运动 3…

【Pygame】 游戏开发 基础知识

【Pygame】 第一课 游戏开发 基础知识概述Pygame 的安装Pygame 基础命令pygame.locals 模块pygame.init()pygame.display.set_mode()案例Pygame 显示文字pygame.font.Font()fill()render()blit()pygame.display.update()案例显示英文显示中文概述 Pygame 是一个跨平台的 Pytho…

计算机网络 - 物理层数据链路层大题复习题

文章目录1、在下面给出的TCP/IP层次模型图示中填写空缺处①-⑤的协议名称。2、请写出在OSI的第几层分别处理下面的问题&#xff1f;3、试简述数据链路层的三个基本问题为什么都必须加以解决&#xff1f;4、收发两端之间的传输距离为100km&#xff0c;信号在媒体上的传播速率为2…

网络层协议数据单元-N_PDU

诊断协议那些事儿 关联文章&#xff1a; UDS的OSI模型 ISO14229各Part介绍&#xff1a;第三部分&#xff1a;CAN实施的统一诊断服务&#xff08;UDSonCAN&#xff09; UDS诊断网络层ISO15765-2&#xff08;CAN&#xff09; 功能寻址的注意事项 文章目录诊断协议那些事儿一、N_…

雅思词汇真经单词共3663个

雅思词汇真经 / Vocabulary for IELTS / 学为贵 赢未来 / 英语真经派学习法 一本书精通雅思词汇 / 刘洪波 编著 / 涵盖&#xff1a;雅思必备核心词汇刘洪波老师原创雅思考点词库 逻辑词群记忆法&#xff0c;一群一群记单词&#xff0c;快速备考无负责 时尚插图&#xff0c;趣味…

Golang实现小型CMS内容管理功能(一):Gin框架搭配Gorm实现增删查改功能

我自己开发了一款在线客服系统&#xff0c;最近一直琢磨把客服系统官网做好。因为访客来的人不少&#xff0c;大部分人可能就是看看官网界面就走了&#xff0c;怎样把这些访客留存下来&#xff0c;去测试试用客服系统&#xff0c;是我一直琢磨的问题。 官网是一个企业的门面&a…

PyQT6 pip install (三) 百篇文章学PyQT

本文章是百篇文章学PyQT的第三篇&#xff0c;本文讲述如何使用PIP安装PyQT6&#xff0c;PyQT6在安装过程中会遇到很多问题&#xff0c;博主在本篇文章中将遇到和踩过的坑总结出来&#xff0c;可以供大家参考&#xff0c;希望大家安装顺利。包括 安装、遇到问题的解决方案、怎么…

卷积运算与卷积核DLC

一、卷积运算 在数学上&#xff0c;卷积的定义是&#xff1a;两个函数在反转和位移后的乘积的积分&#xff0c;其公式表现为&#xff1a; 其中称g为过滤器&#xff0c;f为信号。 但是在深度学习中&#xff0c;卷积并不进行反转&#xff0c;而是直接进行逐元素的乘法和加法&…

【读书笔记】人月神话(一)

后续文章都是从本人的知乎博客上搬迁过来。 在软件领域&#xff0c;布鲁克斯博士的《人月神话》是一本关于大型项目管理的经典之作。这本书不仅对每一个软件行业的项目经理(PM)来说是一本必读读物&#xff0c;对每个软件行业的参与者(程序员&#xff0c;测试人员或者是创业者)…

详解c++---类和对象(三)

目录标题拷贝构造函数为什么会有该函数拷贝构造的特性第一个性质的详解第二个性质详解第三个性质的详解什么时候得自己写拷贝构造函数哪些场景会用到拷贝构造函数运算符重载函数为什么会有运算符重载运算符重载的形式>和>的运算符重载和的运算符的重载-和-的运算符重载运…

用anacnda创建虚拟环境用不用指定python版本

用anaconda创建了python虚拟环境以后&#xff0c;想用pycharm指定这个python解释器。 首先&#xff0c;在我创建虚拟环境并未指定python版本的情况下&#xff0c;在使用anaconda prompt的命令 conda create -n envs_name&#xff08;未指定python版本&#xff09;以后&#xff…

智能油井在线监控解决方案,第一时间掌握所有动态

随着物联网、云计算等技术的不断革新&#xff0c;物联网技术已经渗透到工业生产的方方面面。例如智能油井&#xff1b;油田地域广阔&#xff0c;分布着大量各种油井&#xff0c;油井开采设备的连续稳定运行是保证石油开采的首要条件。但是油井生产过程中&#xff0c;各井之间距…

C++ 多线程 线程安全队列设计

这是看《C并发编程实战》这本书学的&#xff0c;这里我要为这本书辟谣一下&#xff0c;虽然是这本书前面翻译得很烂&#xff0c;但是从第6章开始&#xff0c;应该是换了个人翻译&#xff0c;虽然还是能难懂&#xff0c;但是难懂的是代码逻辑&#xff0c;而不是语言逻辑&#xf…

MySQL8.0优化 - 锁 - 全局锁、死锁

文章目录学习资料锁的不同角度分类锁的分类图如下其他锁之&#xff1a;全局锁其他锁之&#xff1a;死锁概念产生死锁的必要条件如何处理死锁如何避免死锁学习资料 【MySQL数据库教程天花板&#xff0c;mysql安装到mysql高级&#xff0c;强&#xff01;硬&#xff01;-哔哩哔哩…

Java——继承下的抽象类与接口

文章目录壹、抽象类贰、接口前言&#xff1a; 我们前一章写了继承性&#xff0c;这一章节&#xff0c;一起来学习Java中的抽象类与接口相关知识。 壹、抽象类 1、定义&#xff1a;用关键字abstract修饰的类称为抽象类&#xff08;abstract类&#xff09;。 2、格式&#xff1…

进程调度例题解析

文章目录例题1 一个四道作业的操作系统中&#xff0c;设在一段时间内先后到达6个作业&#xff0c;它们的提交时间和运行时间见表例题2 一个具有两道作业的批处理系统&#xff0c;作业调度采用短作业优先的调度算法&#xff0c;进程调度采用以优先数为基础的抢占式调度算法&…

智慧矿山解决方案-最新全套文件

智慧矿山解决方案-最新全套文件一、建设背景二、建设思路智能矿山建设存在的问题1、行业上存在的问题2、承建商存在的问题3、矿井自身存在的问题三、建设方案四、获取 - 智慧矿山全套最新解决方案合集一、建设背景 采矿业是我国国民经济的基础和支柱产业&#xff0c;而矿山智慧…

云安全防护总体架构设计

安全需求和挑战 从风险管理的角度讲&#xff0c;主要就是管理资产、威胁、脆弱性 和防护措施及其相关关系&#xff0c;最终保障云计算平台的持续安全&#xff0c;以及 其所支撑的业务的安全。 云计算 平台是在传统 IT技术的基础上&#xff0c;增加了一个虚拟化层&#xff0c;并…

MobaXterm工具使用/Docker安装Redis/Redisinsight工具使用

✨✨个人主页:沫洺的主页 &#x1f4da;&#x1f4da;系列专栏: &#x1f4d6; JavaWeb专栏&#x1f4d6; JavaSE专栏 &#x1f4d6; Java基础专栏&#x1f4d6;vue3专栏 &#x1f4d6;MyBatis专栏&#x1f4d6;Spring专栏&#x1f4d6;SpringMVC专栏&#x1f4d6;SpringBoot专…

c++——map和set的使用

目录 一. 关联式容器 二. 键值对 三. 树形结构 Ⅰ. set 1. set的介绍 2. set的模版参数 3. set的构造 4. set的迭代器 5. set的容量 6. set其他操作 7. set的使用代码 Ⅱ. map 1. map的介绍 2. map的模板参数说明 3. map的构造 4. map的迭代器 5. map的容量 6…