Mybatis源码解析(六):缓存执行器操作流程

news2024/12/29 8:19:56

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/20862.html

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

相关文章

【附源码】Python计算机毕业设计图书共享系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Bootstrap(三)

目录&#xff1a; &#xff08;1&#xff09;bootstrap响应式工具 &#xff08;2&#xff09;bootstrap实例导航 &#xff08;1&#xff09;bootstrap响应式工具 class加属性&#xff1a; visible-xs&#xff1a;隐藏显示&#xff0c;当屏幕宽度小于等于xs的时候显示、 vis…

如何使用轻量应用服务器搭建Typecho个人博客系统?

之前有写过WordPress搭建博客的教程&#xff1a;如何使用轻量应用服务器搭建WordPress个人博客 有的小伙伴感觉WordPress比较臃肿&#xff0c;有没有比较简洁的博客系统呢&#xff0c;今天就把Typecho搭建个人博客的操作方法分享给大家&#xff0c;需要的小伙伴可以参考以下。 …

注解的使用

1. 注解概述 1.1 注解概述、作用 Java 注解&#xff08;Annotation&#xff09;又称 Java 标注&#xff0c;是 JDK5.0 引入的一种注释机制。Java 语言中的类、构造器、方法、成员变量、参数等都可以被注解进行标注。 1.2 注解的作用是什么 2. 自定义注解 2.1 自定义注解 —…

大数据之Hive基本查询

文章目录前言一、Hive基本查询&#xff08;一&#xff09;普通查询&#xff08;二&#xff09;Join查询&#xff08;三&#xff09;排序查询总结前言 #博学谷IT学习技术支持# 这一次主要介绍Hive的基本查询以及相关函数&#xff0c;Hive的查询以及函数用法与MySQL不尽相同&…

自动化立体仓库系统实训

实训目的 理解被控对象&#xff0c;控制系统关系&#xff1b;逻辑流程任务综合训练&#xff1b;仓储智能管理实现&#xff1b;工业系统整体调试&#xff1b; 实训设备 虚拟仿真被控对象&#xff1b;S7-300控制器及实验台接口&#xff1b;DAQ信号板卡及继电器模块&#xff1b; 设…

基于划分的方法、K-均值算法、K-medoids、K-prototype(机器学习)

目录 基于划分的方法 K-均值算法 k-均值算法聚类步骤如下&#xff1a; K-均值算法优缺点 K-medoids算法 K-prototype算法 基于划分的方法 1、基于划分的方法是简单、常用的一种聚类方法&#xff1b; 2、通过将对象划分为互斥的簇进行聚类&#xff0c; 每个对象属于且仅属…

Python学习基础笔记九——集合

集合&#xff1a;可变的数据结构&#xff0c;元素必须是不可变的数据类型&#xff0c;无序&#xff0c;不重复&#xff08;去重&#xff09;。 数据集的方法&#xff1a; 方法功能S.add(tom)增加一个元素S.update(abc)增加一个可迭代元素&#xff0c;每个元素都分别添加进去S.…

[附源码]java毕业设计网上博物馆设计

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

《计算机视觉技术与应用》-----第五章 边缘和轮廓

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目…

【人工智能】MindSpore Hub

目录 前言 一、什么是MindSpore Hub 1.简单介绍 2.MindSpore Hub包含功能 3.MindSpore Hub使用场景 二、安装MindSpore Hub 1.确认系统环境信息 2.安装 3.下载源码 4.进行验证 三、加载模型 1.介绍 2.推理验证 3.迁移学习 四、模型发布 前言 MindSpore着重提升易…

营造激发自驱力注重培养学习力的想法一

目录背景过程第一节&#xff1a;第二节&#xff1a;第三节&#xff1a;总结升华背景 小编做的是教育类公司&#xff0c;其实无论是做公司的产品&#xff0c;还是对于公司团队人员的培养&#xff0c;都需要去思考教育这件事&#xff0c;尤其是激发自驱力培养学习力&#xff1b;…

常用的框架07-消息中间件-RabbitMQ

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录1.消息中间件概述1.1 为什么学习消息队列1.2 什么是消息中间件1.3 消息队列应用场景1.3.1 异步处理1.3.2 应用程序解耦合1.3.3 削峰填谷1.3.4 什么是QPS1.3.5 什么是…

servlet和vue的增删改查

1.servlet实现步骤 Servlet->新增 servlet获取请求参数&#xff0c;将参数转化为对象&#xff0c;调用service WebServlet("/addService") public class addAllService extends HttpServlet {private BrandService brandService new BrandServiceimpl() ;Over…

云计算之虚拟化技术学习(KVM/Xen/Hyper-V/VMware)

文章目录虚拟化技术什么是虚拟化服务器虚拟化cpu的虚拟化内存虚拟化管理硬盘的虚拟化网络虚拟化IO虚拟化Intel虚拟化技术主流的虚拟化技术虚拟化技术对比XenKVMHyper-VVMware ESX/ESXi虚拟化服务平台Libvirt基于KVM的虚拟化服务平台虚拟化技术 什么是虚拟化 虚拟化是云计算的…

最长公共子序列长度

求两个字符串的最长公共子序列长度。 输入格式: 输入长度≤100的两个字符串。 输出格式: 输出两个字符串的最长公共子序列长度。 输入样例1: ABCBDAB BDCABA输出样例1: 4输入样例2: ABACDEF PGHIK输出样例2: 0 (1条消息) HBU训练营【动态规划DP】——最长公共子序列长…

力扣(LeetCode)799. 香槟塔(C++)

动态规划 设 iii 是行 , jjj 是列 &#xff0c; f[i][j]f[i][j]f[i][j] 表示经过杯子的酒量 &#xff0c;初始 f[0][0]pouredf[0][0]pouredf[0][0]poured &#xff0c; 为了理解&#xff0c;当做每个杯子有无限容量。 当香槟溢出时&#xff0c;f[i][j]f[i][j]f[i][j] 保留自己的…

放大镜-第12届蓝桥杯Scratch选拔赛真题精选

[导读]&#xff1a;超平老师计划推出Scratch蓝桥杯真题解析100讲&#xff0c;这是超平老师解读Scratch蓝桥真题系列的第80讲。 蓝桥杯选拔赛每一届都要举行4~5次&#xff0c;和省赛、国赛相比&#xff0c;题目要简单不少&#xff0c;再加上篇幅有限&#xff0c;因此我精挑细选…

SpringCloud系列(一)Eureka 注册中心

本文主要介绍 Eureka 用来做什么&#xff1f; 如何搭建以及测试&#xff1b;  微服务框架区分于普通的单体架构项目&#xff0c;它是一种经过良好架构设计的分布式架构方案&#xff0c;根据业务功能对系统进行拆分&#xff0c;将每个业务模块都当做是一个独立的项目进行开发&a…

session共享问题及四种解决方案-前端存储、session的复制 、session粘性、后端存储(Mysql、Redis等)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a; 才疏学浅的木子 &#x1f647;‍♂️ 本人也在学习阶段如若发现问题&#xff0c;请告知非常感谢 &#x1f647;‍♂️ &#x1f4d2; 本文来自专栏&#xff1a; 常用工具类以及常见问题处理方法 &#x1f308; 每日一语&…