【MyBatis 源码拆解系列】JVM 级别缓存能力设计:MyBatis 的一、二级缓存如何设计?

欢迎关注公众号 【11来了】 ,持续 MyBatis 源码系列内容!

在我后台回复 「资料」 可领取编程高频电子书
在我后台回复「面试」可领取硬核面试笔记

文章导读地址:点击查看文章导读!

感谢你的关注!

MyBatis 源码系列文章:
(一)MyBatis 源码如何学习?
(二)MyBatis 运行原理 - 读取 xml 配置文件
(三)MyBatis 运行原理 - MyBatis 的核心类 SqlSessionFactory 和 SqlSession
(四)MyBatis 运行原理 - MyBatis 中的代理模式
(五)MyBatis 运行原理 - 数据库操作最终由哪些类负责?
(六)MyBatis 运行原理 - 执行 Mapper 接口的方法时,MyBatis 怎么知道执行的哪个 SQL?

JVM 级别缓存能力设计:MyBatis 的一、二级缓存如何设计?

MyBatis 内部有一级缓存和二级缓存的功能,平常我们也知道他的概念:

  • 一级缓存是 SqlSession 级别的
  • 二级缓存是跨 SqlSession 级别的

但是,还有在实现的过程中还有一些更加细节的内容:缓存生命周期如何设计?什么样的情况下,缓存会失效?如何判断两个 SQL 之间的缓存是否相同?…

因此,接下来会介绍 MyBatis 内部的一、二级缓存如何设计,如何设计不同的类进行分工合作来完成这项缓存工作,这样在以后设计一些本地缓存,都可以借鉴对应的思想!

由于在执行 SQL 查询时,会先查询二级缓存,再查询一级缓存,因此会先介绍二级缓存的实现原理

背景知识

MyBatis 一级缓存默认开启,二级缓存默认关闭

MyBatis 开启二级缓存需要两个步骤:

步骤1:在 mybatis-config.xml 文件中增加配置 cacheEnabled

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><settings><setting name="cacheEnabled" value="true"/></settings><!--...-->
</configuration>

步骤2:在 UserMapper.xml 中增加缓存标签 <cache/>

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.yeecode.mybatisdemo.UserMapper"><cache/><!--...-->
</mapper>

步骤3:在实体类 User 中添加序列化 Serializable

public class User implements Serializable {// ...
}

CacheKey

MyBatis 内部缓存都是以 CacheKey 作为 key,因为获取 SQL 的缓存 需要多个条件 来作为唯一标识

如果仅仅使用 SQL 语句作为缓存的 key,会发生什么?

比如使用 SELECT * FROM user where id > ? 作为 key,那么当传入 id 不同,缓存也肯定不同,如果确定 id = 5,使用 SELECT * FROM user where id > 5 作为 key,会导致缓存的 key 粒度被 固定死 ,那么如果同一条 SQL 可能因为某些其他参数的不同导致查询的结果不同,这种场景就无法实现了

因此使用 CacheKey 可以根据传入条件的不同,来动态调整缓存的粒度

因此 MyBatis 包装了 CacheKey 来作为缓存的 key,内部包含了多个参数条件:包括 MappedStatement 的 id、分页参数、原始 SQL、入参等等 ,创建 CacheKey 的方法在 BaseExecutor 中:

// BaseExecutor # createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {CacheKey cacheKey = new CacheKey();cacheKey.update(ms.getId());cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());cacheKey.update(boundSql.getSql());// ...// cacheKey 还会关联上运行时参数值return cacheKey;
}

在 update() 方法中就会去 关联 对应的一个个参数,接下来进入 update() 方法:

// CacheKey # update
public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;updateList.add(object);
}

在 update() 方法中,入参就是影响缓存唯一标识的变量,比如关联 MappedStatement 的 id ,会将该变量的 hashCode 也加入到当前 CacheKey 的 hashCode 中,并且通过 count、checksum、baseHashCode 来记录影响 CacheKey 的因素

如下,如果 CacheKey 相同,则说明是同一份缓存数据,这里重写了 equals() 方法,比较了 hashcode、checksum、count 等等多种变量,通过多种比较方式来避免 CacheKey 相等的误判

// CacheKey # equals
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) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}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;
}

二级缓存能力

二级缓存的能力是通过 CachingExecutor 来提供的

在执行 UserMapper.xml 中的 SQL 时,会走到 DefaultSqlSession 中,之后又会将 SQL 的执行交给对应的 Executor 来完成,如下图:

// DefaultSqlSession # selectList
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);}
}

这里的 executor 就是 CachingExecutor ,CachingExecutor 就是普通 Executor的 装饰器 ,普通的 Executor 完成 SQL 的一些执行操作,而 CachingExecutor 在这个基础上,可以进行增强,这里增加了 二级缓存 的功能

// CachingExecutor # query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 1、获取 CacheKeyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 2、执行查询操作  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 3、从 MappedStatement 中获取 Cache 对象Cache cache = ms.getCache();if (cache != null) {// 4、判断是否需要清空二级缓存的数据  flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 5、通过 TransactionalCacheManager 去获取对应的二级缓存数据List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {// 6、利用提供查询能力的 Executor 去查询数据  list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); }return list;}}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

步骤 1 会先获取 CacheKey,这个 CacheKey 就是缓存的 key ,因为在比较一个 SQL 是不是同一条 SQL 需要多个条件,将这些条件全部放入 CacheKey 中进行统一比较(详细内容在上边 CacheKey 中已经介绍)

可以发现在 步骤 3 中会从 MappedStatement 中获取一个 Cache 对象,这个 Cache 对象就是在构建 MappedStatement 时放入进去的(MappedStatement 是解析 xml 时创建的,因此说二级缓存的生命周期是 跨 SqlSession 的),也就是在 XMLMapperBuildercacheElement() 中去创建 Cache 对象并且在构建 MappedStatement 对象时,放入进去的(这里就不看详细代码了)

步骤 4 会去判断是否需要清空二级缓存的数据,清空的判断很简单,判断 flushCacheRequired 状态是否为 true,select 标签默认为 false,insert|update|delete 默认为 true,因此修改语句进入到这里都会去清空二级缓存:

// CachingExecutor # flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);}
}

步骤 5 中会去 TransactionalCacheManager 中获取对应的二级缓存数据:

  • TransactionalCacheManager :二级缓存管理器
  • TransactionalCache :在 TransactionalCacheManager 内部,用于存储二级缓存数据

这两个类之间的关系为:

public class TransactionalCacheManager {private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}

TransactionalCacheManager 内部使用 MappedStatement 内部的 Cache 对象作为 key,value 为 TransactionalCache,在该 TransactionalCache 内部就包含了 MappedStatement 里的 Cache 对象,这里 TransactionalCache 也是作为了一个 装饰器类 ,对基本的 Cache 做一个增强,提供了一个事务的能力,会先将查询到的数据暂存起来,当事务提交之后,再把对应的数据放在二级缓存中(详细可以查看 TransactionalCache 内部源码)

如果缓存中没有获取到对应数据,就会在 步骤 6 中去通过内部的 Executor 去查询对应的数据(因为 CachingExecutor 是对其他 Executor 的装饰器,只提供二级缓存能力,真正查询数据的能力还是由其他 Executor 提供)

在默认情况下,在 步骤 6 中会进入到 BaseExecutor 中,去进行真正数据的查询

测试二级缓存

测试使用二级缓存,代码如下,创建两个 SqlSession,分别去执行查询,看代码是否走到缓存中:

// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory =new SqlSessionFactoryBuilder().build(inputStream);User userParam = new User();
userParam.setSchoolName("Sunny School");// 创建 SqlSession
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();UserMapper userMapper1 = session1.getMapper(UserMapper.class);
UserMapper userMapper2 = session2.getMapper(UserMapper.class);
List<User> users = userMapper1.queryUserBySchoolName(userParam);
// 提交 SqlSession1 中的数据,此时 TransactionalCache 就会将数据真正放入二级缓存中
session1.commit();
List<User> users1 = userMapper2.queryUserBySchoolName(userParam);

上边代码中,需要调用 session1.commit() 去提交 SqlSession1 中的事务,之后第二次查询才可以获取到对应的二级缓存,如果去掉该行代码,就无法获取二级缓存了,这是为什么呢?

上边讲了,二级缓存数据是放在 TransactionalCache 中的,而 TransactionalCache 是一个装饰器,提供了增强的事务功能,当查询到数据之后,数据不会立即放入内部的 Cache 二级缓存中,而是先放入到 TransactionalCache 内部的 entriesToAddOnCommit 集合中,当事务提交之后,也就是调用 session1.commit() 之后,数据才会从这个临时集合放入到二级缓存中去

// TransactionalCache # commit 事务提交
public void commit() {if (clearOnCommit) {delegate.clear();}// 将数据放入到二级缓存flushPendingEntries();reset();
}private void flushPendingEntries() {// 遍历 entriesToAddOnCommit 的数据,放入到二级缓存中,delegate 就是【被装饰类】,即存放二级缓存的 Cache 对象for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}
}

怎么判断到底是否走到了二级缓存呢?

二级缓存是 CachingExecutor 提供的能力,因此直接进入到 CachingExecutor 的 query() 方法,直接 debug 就可以看到,如下图,通过 tcm(TransactionalCacheManager) 获取的 list 就是二级缓存数据:

image-20240926145021449

至此,二级缓存能力就已经介绍完毕了

一级缓存能力

一级缓存能力: 一级缓存是 SqlSession 级别的,在一个事务中,如果出现重复的查询结果,就直接使用一级缓存,不去重复的进行查询;并且当执行 嵌套查询时 ,如果一级缓存已经有嵌套查询的结果,也会直接从缓存获取

由于 一级缓存 能力是不可关闭的,因此一级缓存作为一个基础能力被封装在了 BaseExecutor 中,而不是作为一个增强功能放在装饰器类中了

在上边 CachingExecutor 的 query() 方法中,如果从二级缓存拿不到数据,就会走到 BaseExecutor 的 query() 方法中:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLExcepList<E> list;try {// 这个是嵌套查询的深度,可以暂且先不看  queryStack++;// 1、从一级缓存获取数据list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 2、从数据库获取数据  list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}return list;
}

在这里 BaseExecutor 维护了一个 PerpetualCache 缓存,命名为 localCache ,这个就是用于存放一级缓存数据的

如果一级缓存中没有数据,则会通过 queryFromDatabase() 方法去数据库查询,查询之后放入到一级缓存 localCache 中

一级缓存的生命周期是 SqlSession 级别的,在哪里可以体现呢?

DefaultSqlSessionFactoryopenSession() 中去创建一个 SqlSession,此时还会去创建一个 Executor,这个 Executor 其实就是 SimpleExecutor,SimpleExecutor 继承自 BaseExecutor,而一级缓存就是在 BaseExecutor 中,因此BaseExecutor 内部的一级缓存生命周期是和 SqlSession 一致的,因此一级缓存是 SqlSession 级别的:

// DefaultSqlSessionFactory # openSession
public SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {// 创建一个 Executorfinal Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);
}

至此,一级缓存能力也介绍完毕了

总结

最后在总结一下,MyBatis 设计了一级缓存、二级缓存,虽然二级缓存我们并不使用,但是可以了解他的一个设计原理,这里总结一下重点内容:

  • 二级缓存:

    • 二级缓存是跨 SqlSession 级别的,因此是在 MappedStatement 中存储,MappedStatement 是解析 xml 文件时构建的,因此生命周期和 MyBatis 是一直的,因此可以跨 SqlSession

    • 二级缓存的能力支持是通过 CachingExecutor 来完成的,用到了 装饰器模式 ,BaseExecutor 提供一些公有的 一级缓存能力 ,BaseExecutor 作为抽象类,提供了一些模版方法供子类实现,这里 SimpleExecutor 继承 BaseExecutor 去实现自己的方法,而 CachingExecutor 又基于 SimpleExecutor 进行包装增强,在不影响原有功能的基础上提供了 二级缓存的能力

    • 二级缓存数据最终存储在了 TransactionalCache 内部,TransactionalCache 又是基于 Cache 的一个装饰器,通过命名就可以看出,该类 TransactionalCache 增强的能力为事务能力,当事务提交之后,才将数据放入到二级缓存中

  • 一级缓存:

    • 一级缓存是 MyBatis 的基础能力,所以封装在了 BaseExecutor 中,所有执行器继承 BaseExecutor 之后都会具有一级缓存的能力
  • CacheKey:

    • 缓存的 Key 通过 CacheKey 来包装,可以灵活控制缓存的粒度,将多个影响缓存 key 的因素全部放在 CacheKey 中去进行判断
image-20240926132310136

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

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

相关文章

Linux内核对连接的组织和全连接队列

一、Linux内核的组织形式 1.1 描述“连接”的结构 TCP协议的特点是面向连接&#xff0c;一个服务端可能会被多个客户端连接&#xff0c;那这些连接也一定会被操作系统组织起来&#xff0c;接下来我们谈一谈在Linux内核中是如何管理这些连接的。 既然要管理这些连接&#xff0c…

vue3中el-input在form表单按下回车刷新页面

摘要&#xff1a; 在input框中点击回车之后不是调用我写的回车事件&#xff0c;而是刷新页面&#xff01; 如果表单中只有一个input 框则按下回车会直接关闭表单 所以导致刷新页面 再写一个input 表单 &#xff0c;并设置style“display:none” <ElInput style"display…

SkyWalking 高可用

生产环境中,后端应用需要支持高吞吐量并且支持高可用来保证服务的稳定,因此需要高可用集群管理。 集群方案 Skywalking集群是将 skywalking oap 作为一个服务注册到nacos上,只要skywalking oap服务没有全部宕机,保证有一个skywalking oap在运行,就可以提供服务。 高可用…

鸿蒙应用开发前置学习-TypeScript

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

数据安全新攻略!4大神技在手,固态硬盘数据恢复秒变Easy Mode

现在咱们都离不开电脑和手机了&#xff0c;里面存了好多重要的东西&#xff0c;比如学习资料、工作文件&#xff0c;还有照片、视频这些宝贵的记忆。但是有时候数据可能会不小心弄丢或者删掉&#xff0c;特别是在固态硬盘上的数据&#xff0c;要是没了&#xff0c;想找回来比老…

Sui主网升级至V1.34.2

Sui主网现已升级至V1.34.2版本&#xff0c;同时协议升级至60版本。其他升级要点如下所示&#xff1a; 协议 #19014: 在验证Groth16 zk-proof时对无效公共输入进行快速判断。添加了一个新的Move函数flatten&#xff0c;可将向量中的向量展平成单个向量&#xff0c;这在新协议…

爬虫——爬取小音乐网站

爬虫有几部分功能&#xff1f;&#xff1f;&#xff1f; 1.发请求&#xff0c;获得网页源码 #1.和2是在一步的 发请求成功了之后就能直接获得网页源码 2.解析我们想要的数据 3.按照需求保存 注意&#xff1a;开始爬虫前&#xff0c;需要给其封装 headers {User-…

计算机网络:计算机网络体系结构 —— OSI 模型 与 TCP/IP 模型

文章目录 计算机网络体系结构OSI 参考模型TCP/IP 参考模型分层的必要性物理层的主要问题数据链路层的主要问题网络层的主要问题运输层的主要问题应用层的主要问题 分层思想的处理方法发送请求路由器转发接受请求发送响应接收响应 计算机网络体系结构 计算机网络体系结构是指将…

12.Velodyne16线激光雷达在ROS下的仿真(使用 URDF 描述和 Gazebo 插件来模拟 Velodyne 激光扫描仪)

1 下载VLP16的模型描述文件 在这个网站上进行下载&#xff1a; Bitbuckethttps://bitbucket.org/DataspeedInc/velodyne_simulator/src/master/ 使用 URDF 描述和 Gazebo 插件来模拟 Velodyne 激光扫描仪&#xff01; 下图是一个官方给的效果。 URDF with colored meshe…

嵌入式外设应用(代码)

文章目录 1. 工业自动化2. 智能家居设备3. 汽车电子4. 生命体征监测仪5. 物联网应用嵌入式外设应用广泛,有很多应用领域: 1. 工业自动化 应用场景:使用传感器监测设备状态,控制电机的启动和停止。 示例代码: #include <stdio.h> #include <stdbool.h>// 模…

Android 日志打印频率过高排查的一些技巧

最近项目快到 sop 阶段了&#xff0c;看到最近的一个新的任务&#xff0c;提示应用打印频率每秒超过 100 行/秒&#xff0c;需要优化一下。 那这样看起来需要删减一点日志&#xff0c;是不是先要找一下我们的应用打印了多少。 当然如果项目是自己维护的&#xff0c;那肯定是知…

java中创建不可变集合

一.应用场景 二.创建不可变集合的书写格式&#xff08;List&#xff0c;Set&#xff0c;Map) List集合 package com.njau.d9_immutable;import java.util.Iterator; import java.util.List;/*** 创建不可变集合:List.of()方法* "张三","李四","王五…

Let‘s Encrypt 的几个常用命令

Lets Encrypt 是免费的 ssl 证书提供商&#xff0c;在当前纷纷收费的形式下&#xff0c;这是一个良心厂家&#xff0c;虽然使用起来略微繁琐。坚决抵制某 cxxn 站&#xff0c;竟然开始有辣么多收费的东西。这里记录几个常用的命令&#xff08;使用环境Ubuntu 24&#xff09;&am…

MySQL高阶2041-面试中被录取的候选人

目录 题目 准备数据 分析数据 总结 题目 编写解决方案&#xff0c;找出 至少有两年 工作经验、且面试分数之和 严格大于 15 的候选人的 ID 。 可以以 任何顺序 返回结果表。 准备数据 Create table If Not Exists Candidates (candidate_id int, name varchar(30), yea…

给大家提个醒!!!

前些天在某鱼买了一个KNX路由器&#xff0c;外观看起没什么问题&#xff0c;但内部就大跌眼镜了。 话不多说&#xff0c;直接上图 拿到手&#xff0c;外壳看起来没有什么问题 . 上电只亮灯 之后插网线&#xff0c;路由器上找不到设备 开壳&#xff0c;惊掉下巴 加个PHY…

利用自动化工具增强防火墙管理

在选择下一代防火墙以平衡安全需求和网络性能时&#xff0c;组织应优先考虑哪些因素&#xff1f; 最重要的部分——安全需求、可用性和网络性能必须保持平衡&#xff0c;而找到共同点并不总是那么容易。 选择防火墙时&#xff0c;组织必须采取的第一步是深入了解现有网络基础…

广联达 Linkworks办公OA Service.asmx接口存在信息泄露漏洞

漏洞描述 广联达科技股份有限公司以建设工程领域专业应用为核心基础支撑&#xff0c;提供一百余款基于“端云大数据”产品/服务&#xff0c;提供产业大数据、产业新金融等增值服务的数字建筑平台服务商。广联达OA存在信息泄露漏洞&#xff0c;由于某些接口没有鉴权&#xff0c…

基于 STM32F407 的串口 IAP

目录 一、概述二、IAP 实现三、IAP 程序1、串口部分2、iap 程序3、内部 flash 读写4、main 程序 IAP&#xff08;In Application Programming&#xff0c;在应用编程&#xff09;是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写。简单来说&#xff0c;就是开发者…

红外画面空中目标检测系统源码分享

红外画面空中目标检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Comp…

Spring Boot助力IT领域交流平台开发

2 系统关键技术 2.1 JAVA技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xff0c;…