MyBatis 缓存机制源码深度解析:一级缓存与二级缓存

MyBatis 缓存机制源码深度解析:一级缓存与二级缓存

  • 一、一级缓存
    • 1.1 逻辑位置与核心源码解析
    • 1.2 一级缓存容器:PerpetualCache
    • 1.3 createCacheKey 方法与缓存命中
    • 1.4 命中与失效时机
    • 1.5 使用方式
  • 二、二级缓存
    • 2.1 逻辑位置与核心源码解析
    • 2.2 查询流程、命中与失效时机
    • 2.3 使用方式
  • 三、一级缓存与二级缓存的差异

在这里插入图片描述
Java 开发领域,MyBatis 作为主流的持久层框架,其缓存机制是提升系统性能的关键利器。MyBatis 提供的一级缓存二级缓存,通过不同的策略与实现,有效减少数据库访问次数。
本文基于 MyBatis 3.4.6 版本,结合关键源码,深入解析两种缓存的原理、使用及差异。

一、一级缓存

1.1 逻辑位置与核心源码解析

一级缓存又称会话级缓存,其核心逻辑主要存在于org.apache.ibatis.executor.BaseExecutor类中。BaseExecutorMyBatis 执行器的基础抽象类,负责管理一级缓存相关操作。最外层执行的query方法,包含了缓存的核心逻辑,而doQuery方法则是具体的数据库查询操作,由BaseExecutor的子类(如SimpleExecutorReuseExecutor等)实现。

 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 构造缓存keyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 执行查询逻辑return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
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());// ...List<E> list;try {queryStack++;// 优先从一级缓存(localCache)中获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 处理存储过程的输出参数缓存handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 一级缓存未命中,执行数据库查询(queryFromDatabase有具体的数据库查询逻辑)list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}// ...return list;
}
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);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}

上述代码中,query方法先判断缓存相关条件,尝试从localCache获取数据。
若缓存未命中,则调用queryFromDatabase方法执行数据库查询,并将结果存入一级缓存。

1.2 一级缓存容器:PerpetualCache

一级缓存的数据存储在org.apache.ibatis.cache.PerpetualCache类实例中。PerpetualCache是一个基于 HashMap 实现的简单缓存类,用于存储缓存数据。

public class PerpetualCache implements Cache {private final String id;// 使用HashMap存储缓存数据,key为缓存键,value为缓存值private Map<Object, Object> cache = new HashMap<Object, Object>();public PerpetualCache(String id) {this.id = id;}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}// ...
}

PerpetualCache通过putObjectgetObject等方法实现对缓存数据的操作,BaseExecutor通过操作该实例来管理一级缓存。

1.3 createCacheKey 方法与缓存命中

CacheKey是缓存的唯一标识,MyBatis 通过createCacheKey方法生成CacheKey对象。该方法位于org.apache.ibatis.executor.BaseExecutor类,其核心逻辑是将 SQL 语句、参数、RowBounds 等信息进行哈希计算,生成唯一的缓存键。

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// 设置Mapper语句的唯一标识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 logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;String propertyName = parameterMapping.getProperty();// 优先从SQL绑定参数中获取值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) {// 添加环境ID到CacheKey中cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}

当再次执行相同 SQL 查询时,若生成的CacheKey与缓存中已存在的CacheKey一致,且在同一个SqlSession内,就会触发一级缓存命中,直接从缓存获取数据。

1.4 命中与失效时机

  • 命中时机:在同一个SqlSession中,执行的 SQL 语句、输入参数完全相同(即生成的CacheKey相同)时,一级缓存命中。

  • 失效时机SqlSession关闭,或执行insertupdatedelete操作时,一级缓存会失效。以update操作的源码为例,在org.apache.ibatis.executor.BaseExecutor类的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);
    }
    

    执行update操作时,会调用clearLocalCache方法清空当前SqlSession的一级缓存,保证数据一致性。insertdelete操作也有类似逻辑。

1.5 使用方式

一级缓存默认开启,无需额外配置。在同一个SqlSession中,MyBatis 会自动管理缓存的读写与失效。以下是一个简单的 demo 案例代码:

public class CacheTest {private SqlSessionFactory sessionFactory;/*** 加载配置文件。并且初始化SqlSessionFactory*/@Beforepublic void before() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);}@Testpublic void testGetById() {SqlSession sqlSession = sessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// 第一次查询,会执行数据库查询User user1 = userMapper.getUserById(6);// 第二次查询,由于在同一个SqlSession且查询条件相同,会命中一级缓存User user2 = userMapper.getUserById(6);System.out.println(user1 == user2); // 输出true}
}

在这里插入图片描述

二、二级缓存

2.1 逻辑位置与核心源码解析

  • MyBatis 执行 SQL 时,整体流程是先经过CachingExecutor(最外层),最后才是其他ExecutorBaseExecutor子类)
  • CachingExecutor作为二级缓存的核心执行者,采用适配器模式,内部持有一个Executor对象(delegate),该delegate由具体的子类执行器(如SimpleExecutor)实例化,负责执行具体的数据库操作*
  • MyBatis 默认未开启二级缓存,需要在配置文件和映射文件中进行配置才能启用。二级缓存又称全局缓存,其核心逻辑存在于org.apache.ibatis.executor.CachingExecutor
public class CachingExecutor implements Executor {private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}@Overridepublic <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) {// 处理可刷新缓存的情况,如执行增删改操作后需要刷新缓存flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 优先从二级缓存中获取结果List<E> list = (List<E>) tcm.getObject(cache, key);if (list!= null) {return list;}}}// 二级缓存未命中,委托给具体的执行器执行查询return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}// 省略其他方法...
}

CachingExecutorquery方法中,首先通过ms.getCache()获取当前 Mapper 语句对应的缓存对象,判断二级缓存是否开启。
若开启且可用,则尝试从二级缓存获取数据。
若未命中,则将查询委托给delegate,由delegate(如SimpleExecutor)调用BaseExecutor的相关方法执行具体数据库查询操作,并在事务提交后将结果写入二级缓存。

2.2 查询流程、命中与失效时机

  • 查询流程MyBatis 先在二级缓存中查找CacheKey对应的结果,若未命中,再检查一级缓存,若一级缓存也未命中,则执行数据库查询,查询结果先存入一级缓存,事务提交后存入二级缓存。​

  • 命中时机namespace相同,执行的 SQL 语句和输入参数相同(CacheKey相同),且事务已提交,数据已写入二级缓存时,二级缓存命中。​

  • 失效时机:执行insertupdatedelete操作,或手动调用方法,会清空当前namespace下的二级缓存。以update操作为例,在CachingExecutor类的update方法中,通过flushCacheIfRequired方法处理缓存刷新

    public int update(MappedStatement ms, Object parameterObject) throws SQLException {flushCacheIfRequired(ms);return delegate.update(ms, parameterObject);
    }
    private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache!= null && ms.isFlushCacheRequired()) {tcm.clear(cache);}
    }
    

    当检测到当前 Mapper 语句配置了需要刷新缓存(ms.isFlushCacheRequired()true),就会通过TransactionalCacheManagerclear方法清空对应的二级缓存,实现缓存失效,保证数据一致性。insertdelete操作也会触发类似的缓存清空逻辑 。

2.3 使用方式

  1. MyBatis 的配置文件中开启二级缓存:

    <configuration><settings><setting name="cacheEnabled" value="true"/></settings>
    </configuration>
    
  2. Mapper映射文件中添加<cache>标签启用二级缓存,并可配置缓存属性:

    <mapper namespace="com.coderzpw.dao.UserMapper"><cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/><select id="getUserById" resultType="com.coderzpw.domain.User">SELECT * FROM user WHERE id = #{id}</select>
    </mapper>
    
  3. 编写测试代码验证二级缓存效果:

    User user1 = null;
    User user2 = null;
    try (SqlSession sqlSession1 = sessionFactory.openSession()) {UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);user1 = userMapper1.getUserById(6);sqlSession1.commit();
    }
    try (SqlSession sqlSession2 = sessionFactory.openSession()) {UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);user2 = userMapper2.getUserById(6);sqlSession2.commit();
    }
    System.out.println(user1 == user2); // 如果readOnly为true,这里会输出true
    

三、一级缓存与二级缓存的差异

对比项一级缓存二级缓存
作用范围SqlSession级别,会话内有效,仅在当前SqlSession中共享namespace级别,全局有效,可在多个SqlSession间共享
开启方式默认开启,无需配置需要在配置文件和映射文件中配置开启
失效机制SqlSession关闭或执行增删改操作时,通过调用clearLocalCache清空缓存执行增删改操作或手动调用方法,通过TransactionalCacheManager清空对应namespace下的缓存
实现核心类BaseExecutorPerpetualCacheCachingExecutorTransactionalCacheManager
数据存储位置存储在SqlSession对应的localCache存储在namespace对应的缓存区域,由TransactionalCacheManager管理

深入理解 MyBatis 一级缓存和二级缓存的原理与使用,有助于开发者根据业务场景灵活配置缓存策略,在提升系统性能的同时,确保数据的一致性与准确性。

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

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

相关文章

【题解-Acwing】1097. 池塘计数

题目&#xff1a;1097. 池塘计数 题目描述 农夫约翰有一片 N∗M 的矩形土地。 最近&#xff0c;由于降雨的原因&#xff0c;部分土地被水淹没了。 现在用一个字符矩阵来表示他的土地。 每个单元格内&#xff0c;如果包含雨水&#xff0c;则用”W”表示&#xff0c;如果不含…

基于Flask框架的前后端分离项目开发流程是怎样的?

基于Flask框架的前后端分离项目开发流程可分为需求分析、架构设计、并行开发、集成测试和部署上线五个阶段。以下是详细步骤和技术要点&#xff1a; 一、需求分析与规划 1. 明确项目边界 功能范围&#xff1a;确定核心功能&#xff08;如用户认证、数据管理、支付流程&#…

板凳-------Mysql cookbook学习 (十--2)

5.12 模式匹配中的大小写问题 mysql> use cookbook Database changed mysql> select a like A, a regexp A; ------------------------------ | a like A | a regexp A | ------------------------------ | 1 | 1 | --------------------------…

编程实验篇--线性探测哈希表

线性探测哈希表性能测试实验报告 1. 实验目的 编程实现线性探测哈希表。编程测试线性探测哈希表。了解线性探测哈希表的性能特征&#xff0c;并运行程序进行验证。 2. 实验背景与理论基础 哈希表是一种高效的数据结构&#xff0c;用于实现符号表&#xff08;Symbol Table&a…

使用Python提取PDF元数据的完整指南

PDF文档中包含着丰富的元数据信息&#xff0c;这些信息对文档管理和数据分析具有重要意义。本文将详细介绍如何利用Python高效提取PDF元数据&#xff0c;并对比主流技术方案的优劣。 ## 一、PDF元数据概述 PDF元数据&#xff08;Metadata&#xff09;是包含在文档中的结构化信…

【量化】策略交易类型

通过查找相关资料&#xff0c;这里罗列了一些常见的策略交易类型&#xff0c;如下&#xff1a; &#x1f4ca; 技术分析类策略 均线交叉策略&#xff08;SMA、EMA&#xff09;动量策略&#xff08;Momentum&#xff09;相对强弱指数策略&#xff08;RSI&#xff09;随机指标策…

【Go语言基础【17】】切片:一种动态数组

文章目录 零、概述一、切片基础1、切片的结构2、切片的创建方式3、切片的操作与扩容 二、切片的拷贝与共享内存三、切片作为函数参数 Go语言的切片&#xff08;slice&#xff09;是一种动态数组&#xff0c;提供了灵活、高效的元素序列操作。它基于底层数组实现&#xff0c;通过…

MybatisPlus使用DB静态工具出现找不到实体类的报错

报错&#xff1a;Not Found TableInfoCache. 原因在于没有创建实体类对应的mapper&#xff0c;并且该mapper还必须继承baseMapper。 猜测大概的原理应该是DB会去查找实体类对应的mapper&#xff0c;然后通过mapper去查找对应的实体类。

Linux nano命令的基本使用

参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时&#xff0c;显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …

LLMs 系列科普文(15)

前面 14 篇文章&#xff0c;就是本系列科普文中想介绍的大部分技术内容。重点讲述了训练这些模型的三个主要阶段和范式&#xff1a;预训练、监督微调和强化学习。 我向你们展示了这些步骤大致对应于我们已用于教导儿童的过程。具体来说&#xff0c;我们将预训练比作通过阅读说…

深入理解汇编语言中的顺序与分支结构

本文将结合Visual Studio环境配置、顺序结构编程和分支结构实现&#xff0c;全面解析汇编语言中的核心编程概念。通过实际案例演示无符号/有符号数处理、分段函数实现和逻辑表达式短路计算等关键技术。 一、汇编环境配置回顾&#xff08;Win32MASM&#xff09; 在Visual Studi…

Selenium4+Python的web自动化测试框架

一、什么是Selenium&#xff1f; Selenium是一个基于浏览器的自动化测试工具&#xff0c;它提供了一种跨平台、跨浏览器的端到端的web自动化解决方案。Selenium主要包括三部分&#xff1a;Selenium IDE、Selenium WebDriver 和Selenium Grid。 Selenium IDE&#xff1a;Firefo…

React 样式方案与状态方案初探

React 本身只提供了基础 UI 层开发范式&#xff0c;其他特性的支持需要借助相关社区方案实现。本文将介绍 React 应用体系中样式方案与状态方案的主流选择&#xff0c;帮助开发者根据项目需求做出合适的选择。 1. React 样式方案 1.1. 内联样式 (Inline Styles) 通过 style …

PHP中如何定义常量以及常量和变量的主要区别

在PHP编程中&#xff0c;常量和变量是存储数据的两种重要方式。常量在定义后值不能改变&#xff0c;而变量的值可以在程序执行过程中发生变化。本文将详细介绍如何在PHP中定义常量&#xff0c;并深入探讨常量和变量的主要区别。 一、PHP中定义常量 1. 使用 define 函数定义常…

奈飞工厂官网,国内Netflix影视在线看|中文网页电脑版入口

奈飞工厂是一个专注于提供免费Netflix影视资源的在线播放平台&#xff0c;致力于为国内用户提供的Netflix热门影视内容。该平台的资源与Netflix官网基本同步&#xff0c;涵盖电影、电视剧、动漫和综艺等多个领域。奈飞工厂的界面简洁流畅&#xff0c;资源分类清晰&#xff0c;方…

CMS内容管理系统的设计与实现:架构设计

一、整体架构方案 &#xff08;一&#xff09;架构方案选择&#xff08;根据项目规模&#xff09; 1. 中小型项目推荐方案&#xff08;团队<10人&#xff09; #mermaid-svg-cjzaHpptY8pYWnzo {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:1…

嵌入式里的时间魔法:RTC 与 BKP 深度拆解

文章目录 RTC实时时钟与BKPUnix时间戳UTC/GMT时间戳转换时间戳转换BKP简介BKP基本结构1. 电池供电模块&#xff08;VBAT 输入&#xff09;2. 侵入检测模块&#xff08;TAMPER 输入&#xff09;3. 时钟输出模块&#xff08;RTC 输出&#xff09;4. 内部寄存器组 RTC简介RTC时钟源…

STC8H系列 驱动步进电机

STC8H 驱动步进电机 一、引言二、硬件设计三、软件设计Step_Motor2.c文件Step_ Motor2.h文件 一、引言 众所周知STC8H系列有两个PWM&#xff0c;分别为PWMA和PWMB外设模块&#xff0c;我全都用上&#xff0c;岂不是就有两个带动电机的脉冲信号&#xff1f;&#xff01;哈哈哈哈…

Python高阶函数:从入门到精通

目录 Python高阶函数详解&#xff1a;从概念到高级应用引言&#xff1a;函数式编程的魅力一、高阶函数基础概念1.1 什么是高阶函数1.2 Python中的一等函数 二、内置高阶函数详解2.1 map函数&#xff1a;数据转换利器2.2 filter函数&#xff1a;数据筛选专家2.3 reduce函数&…

腾讯开源视频生成工具 HunyuanVideo-Avatar,上传一张图+一段音频,就能让图中的人物、动物甚至虚拟角色“活”过来,开口说话、唱歌、演相声!

腾讯混元团队提出的 HunyuanVideo-Avatar 是一个基于多模态扩散变换器&#xff08;MM-DiT&#xff09;的模型&#xff0c;能够生成动态、情绪可控和多角色对话视频。支持仅 10GB VRAM 的单 GPU运行&#xff0c;支持多种下游任务和应用。例如生成会说话的虚拟形象视频&#xff0…