MyBaits 二级缓存原理

news/2025/5/24 1:08:22

优质博文:IT-BLOG-CN

一级缓存原理

默认关闭,一般不建议使用。为什么不建议使用我们要清楚。

先给不建议使用的原因: MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计, 使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们 的命名空间不同, 因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据

一、二级缓存配置

1、配置mybatis核心配置文件

<settings><!--因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置。为true代表开启二级缓存;为false代表不开启二级缓存。--><setting name="cacheEnabled" value="true"/>	
</settings>

2、XML开发方式:xxxMapper.xml映射中配置cache或者cache-ref

<mapper namespace="com.lagou.dao.UserMapper"><!--当前映射文件开启二级缓存--><cache></cache><!--<select>标签中设置useCache=”true”代表当前这个statement要使用二级缓存。如果不使用二级缓存可以设置为false注意:如果每次查询都需要最新的数据sql,要设置成useCache="false",禁用二级缓存。--><select id="findById" parameterType="int" resultType="user" useCache="true">SELECT * FROM `user` where id = #{id}</select>
</mapper>
type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction: 定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size: 最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。(为了解决下述问题)

先看下二级缓存存在的问题:多表联合查询产生脏数据

@Test
public void testCacheWithDiffererntNamespace() throws Exception {SqlSession sqlSession1 = factory.openSession(true); SqlSession sqlSession2 = factory.openSession(true); SqlSession sqlSession3 = factory.openSession(true); StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);System.out.println(studentMapper.getStudentByIdWithClassInfo(1));sqlSession1.close();System.out.println(studentMapper2.getStudentByIdWithClassInfo(1));classMapper.updateClassName("重点一班",1);sqlSession3.commit();System.out.println(studentMapper2.getStudentByIdWithClassInfo(1));
}

执行结果:在这个实验中,我们引入了两张新的表,一张class,一张classroomclass中保存了班级的id和班级名,classroom中保存了班级id和学生id。我们在StudentMapper中增加了一个查询方法getStudentByIdWithClassInfo,用于查询学生所在的班级,涉及到多表查询。在ClassMapper中添加了updateClassName,根据班级id更新班级名的操作。

DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,c.className as className FROM classrome cr JOIN student s ON cr.student_id == s.id JOIN class c ON cl.class_id == c.id WHERE s.id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age, className
TRACE [main] - <==        Row: 1, 小明, 13, 一班
DEBUG [main] - <==      Total: 1
StudentEntity{id=1, name='小明', age=13, className='一班'}
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.5
StudentEntity{id=1, name='小明', age=13, className='一班'}
DEBUG [main] - ==>  Preparing: UPDATE class SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: 重点一班(String) 1(Integer)
DEBUG [main] - <==      Updates: 1
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.66666666666666666
StudentEntity{id=1, name='小明', age=13, className='一班'}

sqlsession1studentmapper查询数据后,二级缓存生效。保存在StudentMappernamespace下的cache中。当sqlSession3classMapperupdateClassName方法对class表进行更新时,updateClassName不属于StudentMappernamespace,所以StudentMapper下的cache没有感应到变化,没有刷新缓存。当StudentMapper中同样的查询再次发起时,从缓存中读取了脏数据。

为了解决上述的问题,可以使用Cache ref,让ClassMapper引用StudenMapper命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。

<cache-ref namespace="mapper.StudentMapper"/>

执行结果:不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,c.className as className FROM classrome cr JOIN student s ON cr.student_id == s.id JOIN class c ON cl.class_id == c.id WHERE s.id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age, className
TRACE [main] - <==        Row: 1, 小明, 13, 一班
DEBUG [main] - <==      Total: 1
StudentEntity{id=1, name='小明', age=13, className='一班'}
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.5
StudentEntity{id=1, name='小明', age=13, className='一班'}
DEBUG [main] - ==>  Preparing: UPDATE class SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: 重点一班(String) 1(Integer)
DEBUG [main] - <==      Updates: 1
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.3333333333333
DEBUG [main] - ==>  Preparing: SELECT s.id,s.name,s.age,c.className as className FROM classrome cr JOIN student s ON cr.student_id == s.id JOIN class c ON cl.class_id == c.id WHERE s.id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age, className
TRACE [main] - <==        Row: 1, 小明, 13, 重点一班
DEBUG [main] - <==      Total: 1
StudentEntity{id=1, name='小明', age=13, className='重点一班'}

注解开发方式: 或者配置Mapper接口,添加注解

@CacheNamespace
public interface UserMapper {...}

二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的,也就是要求实现Serializable接口。

二、二级缓存清除方式

映射文件XML中添加flushCache=“true”

<select flushCache="true"></select>

三、原理分析

在一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
在这里插入图片描述
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

CachingExecutorExecutor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式。

CachingExecutorquery方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache

Cache cache = ms.getCache();

以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。

SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

是否需要刷新缓存,代码如下所示:

flushCacheIfRequired(ms);

在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示:

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

MyBatisCachingExecutor持有了TransactionalCacheManager,即上述代码中的tcmTransactionalCacheManager中持有一个Map,代码如下所示:

private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。

TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。

TransactionalCacheclear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:

@Override
public void clear() {clearOnCommit = true;entriesToAddOnCommit.clear();
}

CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。

if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, parameterObject, boundSql);

之后会尝试从tcm中获取缓存的列表。

List<E> list = (List<E>) tcm.getObject(cache, key);

getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。

Object object = delegate.getObject(key);
if (object == null) {entriesMissedInCache.add(key);
}

CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。

if (list == null) {list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116
}

tcmput方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。

@Override
public void putObject(Object key, Object object) {entriesToAddOnCommit.put(key, object);
}

从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsessioncommit方法中做了什么。代码如下所示:

@Override
public void commit(boolean force) {try {executor.commit(isCommitOrRollbackRequired(force));

因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。

@Override
public void commit(boolean required) throws SQLException {delegate.commit(required);tcm.commit();
}

会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit()tcm最终又会调用到TrancationalCache

public void commit() {if (clearOnCommit) {delegate.clear();}flushPendingEntries();reset();
}

看到这里的clearOnCommit就想起刚才TrancationalCacheclear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:

private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}................
}

flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。

后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutorupdate方法,其中调用了这个函数,代码如下所示:

private void flushCacheIfRequired(MappedStatement ms) 

在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。

MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatisCache接口实现,有一定的开发成本,直接使用RedisMemcached等分布式缓存可能成本更低,安全性也更高。


https://dhexx.cn/news/show-5415244.html

相关文章

Chainlit快速实现AI聊天的API接口提供给小程序或者APP等第三方应用

API接口 您可以将 Chainlit 应用安装到现有 FastAPI 应用上以创建自定义端点。一个很好的用例是通过 rest API 为助手提供服务。Chainlit本身就是前后端分离的服务&#xff0c;只不过是把前端文件打包后集成到Chainlit包内部了&#xff0c;打开Chainlit的聊天网页&#xff0c;…

区块链layer2和layer1的划分依据

区块链 layer2 和 layer1 的划分依据 区块链 layer2 和 layer1 的划分主要依据以下几个方面&#xff1a; 功能和作用 Layer1&#xff1a;主要解决区块链的底层基础架构问题&#xff0c;包括数据层、共识层和激励层等。它确保了区块链的去中心化、安全性和最终状态的确认&…

一、selenium自动化简介selenium工具集

文章目录 一、简介二、组成部分三、selenium工具集3.1 Selenium IDE3.2 Selenium WebDriver3.3 Selenium Grid3.4 Appium 一、简介 官方网站 Selenium 是支持 web 浏览器自动化的一系列工具和库的综合项目。 它提供了扩展来模拟用户与浏览器的交互&#xff0c;用于扩展浏览器分…

[建议收藏]RTSP|RTMP直播流录制下来的MP4文件起播慢怎么办?

MP4文件起播慢原因 我们在做录制的时候&#xff0c;会遇到这样的问题&#xff0c;有的开发者会把直播录制的mp4文件&#xff0c;用来做点播之用&#xff0c;这时候&#xff0c;会发现需要文件下载下来&#xff0c;才可以正常播放&#xff0c;也就我们常说的起播比较慢。 这个…

Java 入门指南:Java 并发编程 —— 并发容器 TransferQueue、LinkedTransferQueue、SynchronousQueue

BlockingQueue BlockingQueue 是Java并发包&#xff08;java.util.concurrent&#xff09;中提供的一个阻塞队列接口&#xff0c;它继承自 Queue 接口。 BlockingQueue 中的元素采用 FIFO 的原则&#xff0c;支持多线程环境并发访问&#xff0c;提供了阻塞读取和写入的操作&a…

智能电气火灾监控探测器织就0.4kV配电系统防火网

在现代化城市的心脏地带&#xff0c;从高耸的智能楼宇到繁忙的工矿企业&#xff0c;电力如同血液般滋养着每一个角落&#xff0c;而电气安全则是这庞大体系中最不容忽视的防线。 电气火灾预防利器——ARCM剩余电流式电气火灾监控探测器&#xff0c;成功入驻0.4kV低压配电系统&…

【JVM】Java内存分配与回收:深入理解Java内存管理

Java内存分配与回收&#xff1a;深入理解Java内存管理 引言 Java虚拟机&#xff08;JVM&#xff09;的内存管理是确保Java应用程序性能和稳定性的关键。理解Java的内存分配方式和回收过程对于开发者来说至关重要。 基础知识 JVM内存模型&#xff1a;JVM内存分为堆&#xff…

文章润色太费时?试试这5款ai写作工具

你是否曾梦想拥有一个私人编辑&#xff0c;随时随地帮你打磨文字&#xff0c;让写作变得既轻松又专业&#xff1f; 告诉你一个好消息&#xff0c;现在有5款AI写作工具&#xff0c;它们就拥有这样的能力&#xff01;这些AI助手擅长润色文章&#xff0c;优化语法&#xff0c;甚至…

分类预测|基于CNN提取特征使用支持向量机作为分类器进行分类预测CNN-SVM(SVM自动寻优c,g超参数)CNN和CNN-SVM

分类预测|基于CNN提取特征使用支持向量机作为分类器进行分类预测CNN-SVM&#xff08;SVM自动寻优c&#xff0c;g超参数&#xff09;CNN和CNN-SVM 文章目录 一、基本原理1. 卷积神经网络&#xff08;CNN&#xff09;简介CNN的基本结构 2. 支持向量机&#xff08;SVM&#xff09;…