图解 | 聊聊 MyBatis 缓存
首发公众号-悟空聊架构:图解 | 聊聊 MyBatis 缓存
你好,我是悟空。
本文主要内容如下:
一、MyBatis 缓存中的常用概念
MyBatis 缓存:它用来优化 SQL 数据库查询的,但是可能会产生脏数据。
SqlSession:代表和数据库的一次会话,向用户提供了操作数据库的方法。
MappedStatement:代表要发往数据库执行的指令,可以理解为是 SQL 的抽象表示。
Executor: 代表用来和数据库交互的执行器,接受 MappedStatment 作为参数。
namespace:每个 Mapper 文件只能配置一个 namespace,用来做 Mapper 文件级别的缓存共享。
映射接口:定义了一个接口,然后里面的接口方法对应要执行 SQL 的操作,具体要执行的 SQL 语句是写在映射文件中。
映射文件:MyBatis 编写的 XML 文件,里面有一个或多个 SQL 语句,不同的语句用来映射不同的接口方法。通常来说,每一张单表都对应着一个映射文件。
二、MyBatis 一级缓存
2.1 一级缓存原理
在一次 SqlSession 中(数据库会话),程序执行多次查询,且查询条件完全相同,多次查询之间程序没有其他增删改操作,则第二次及后面的查询可以从缓存中获取数据,避免走数据库。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
Local Cache 其实是一个 hashmap 的结构:
private Map<Object, Object> cache = new HashMap<Object, Object>();
如下图所示,有两个 SqlSession,分别为 SqlSession1 和 SqlSession2,每个 SqlSession 中都有自己的缓存,缓存是 hashmap 结构,存放的键值对。
键是 SQL 语句组成的 Key :
Statement Id + Offset + Limmit + Sql + Params
值是 SQL 查询的结果:
2.2 一级缓存配置
在 mybatis-config.xml 文件配置,name=localCacheScope,value有两种值:SESSION 和 STATEMENT
<configuration>
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
<configuration>
SESSION:开启一级缓存功能
STATEMENT:缓存只对当前执行的这一个 SQL 语句有效,也就是没有用到一级缓存功能。
首先我们通过几个考题来体验下 MyBatis 一级缓存。
2.3 一级缓存考题
考题(1)只开启了一级缓存,下面的代码调用了三次查询操作 getStudentById,请判断,下列说法正确的是?
// 打开一个 SqlSession
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 根据 id=1 查询学生信息
System.out.println(studentMapper.getStudentById(1));
// 根据 id=1 查询学生信息
System.out.println(studentMapper.getStudentById(1));
// 根据 id=1 查询学生信息
System.out.println(studentMapper.getStudentById(1));
答案:第一次从数据库查询到的数据,第二次和第二次从 MyBatis 一级缓存查询的数据。
解答:第一次从数据库查询后,后续查询走 MyBatis 一级缓存
考题(2)只开启了一级缓存,下面代码示例中,开启了一个 SqlSession 会话,调用了一次查询,然后对数据进行了更改,又调用了一次查询,下列关于两次查询的说法,正确的是?
// 打开一个 SqlSession
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 根据 id=1 查询学生信息
System.out.println(studentMapper.getStudentById(1));
// 插入了一条学生数据,改变了数据库
System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
// 根据 id=1 查询学生信息
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
答案:第一次从数据库查询到的数据,第二次从数据库查询的数据
解答:第一次从数据库查询后,后续更新(包括增删改)数据库中的数据后,这条 SQL 语句的缓存失效了,后续查询需要重新从数据库获取数据。
考题(3)当开启了一级缓存,下面的代码中,开启了两个 SqlSession,第一个 SqlSession 查询了两次学生 A 的姓名,第二次 SqlSession 更新了一次学生 A 的姓名,请判断哪个选项符合最后的查询结果。
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); studentMapper2.updateStudentName("B",1);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper2.getStudentById(1));
答案:
A
B
解答:只开启一级缓存的情况下,SqlSession 级别是不共享的。代码示例中,分别创建了两个 SqlSession,在第一个 SqlSession 中查询学生 A 的姓名,第二个 SqlSession 中修改了学生 A 的姓名为 B,SqlSession2 更新了数据后,不会影响 SqlSession1,所以 SqlSession1 查到的数据还是 A。
2.4 MyBatis 一级缓存失效的场景
- 不同的SqlSession对应不同的一级缓存
- 同一个SqlSession但是查询条件不同
- 同一个SqlSession两次查询期间执行了任何一次增删改操作
- 同一个SqlSession两次查询期间手动清空了缓存
2.5 MyBatis 一级缓存总结
MyBatis一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺
MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement
一级缓存的配置中,默认是 SESSION 级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。
三、MyBatis 二级缓存
3.1 MyBatis 二级缓存概述
MyBatis的二级缓存相对于一级缓存来说,实现了
SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis的Cache 接口实现,有一定的开发成本,直接使用Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
3.2 MyBatis 二级缓存原理
一级缓存最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。
开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询。
二级缓存开启后,同一个 namespace下的所有操作语句,都影响着同一个Cache。
每个 Mapper 文件只能配置一个 namespace,用来做 Mapper 文件级别的缓存共享。
<mapper namespace="mapper.StudentMapper"></mapper>
二级缓存被同一个 namespace 下的多个 SqlSession 共享,是一个全局的变量。MyBatis 的二级缓存不适应用于映射文件中存在多表查询的情况。
通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。
3.3 MyBatis缓存查询的顺序
- 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
- 如果二级缓存没有命中,再查询一级缓存
- 如果一级缓存也没有命中,则查询数据库
- SqlSession关闭之后,一级缓存中的数据会写入二级缓存。
3.4 二级缓存配置
开启二级缓存需要在 mybatis-config.xml 中配置:
<settingname="cacheEnabled"value="true"/>
3.5 二级缓存考题
测试update操作是否会刷新该namespace下的二级缓存。
开启了一级和二级缓存,通过三个SqlSession 查询和更新 学生张三的姓名,判断最后的输出结果是什么?
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);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class); System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1)); studentMapper3.updateStudentName("李四",1);
sqlSession3.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
答案:
张三
张三
李四
解答:三个 SqlSession 是共享 MyBatis 缓存,SqlSession2 更新数据后,MyBatis 的 namespace 缓存(StudentMapper) 就失效了,SqlSession2 最后是从数据库查询到的数据。
四、MyBatis 自定义缓存
4.1 MyBatis 自定义缓存概述
当 MyBatis 二级缓存不能满足要求时,可以使用自定义缓存替换。(较少使用)
自定义缓存需要实现 MyBatis 规定的接口:org.apache.ibatis.cache.Cache。这个接口里面定义了 7 个方法,我们需要自己去实现对应的缓存逻辑。
4.2 整合第三方缓存 EHCache
EHCache 和 MyBatis 已经帮我们整合好了一个自定义缓存,我们可以直接拿来用,不需要自己去实现 MyBatis 的 org.apache.ibatis.cache.Cache 接口。
添加 mybatis-ehcache 依赖包。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
创建EHCache的配置文件ehcache.xml。
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\passjava\ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
设置二级缓存的类型,在xxxMapper.xml文件中设置二级缓存类型
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
4.3 EHCache配置文件说明
五、总结
本篇分别介绍了 MyBatis 一级缓存、二级缓存、自定义缓存的原理和使用,其中还穿插了 4 道考题来验证 MyBatis 缓存的功能。不足之处是 MyBatis 缓存源码未分析。
参考资料:
https://tech.meituan.com/2018/01/19/mybatis-cache.html
关于我
多年互联网摸爬滚打经验,擅长微服务、分布式、架构设计。目前在一家大型上市公司从事基础架构和性能优化工作。
InfoQ 签约作者、蓝桥签约作者、阿里云专家博主、51CTO 红人。
我的所有文章都汇总到这里了 http://www.passjava.cn
图解 | 聊聊 MyBatis 缓存的更多相关文章
- 聊聊MyBatis缓存机制【美团-推荐】
聊聊MyBatis缓存机制 2018年01月19日 作者: 凯伦 文章链接 18778字 38分钟阅读 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用My ...
- 聊聊MyBatis缓存机制
https://tech.meituan.com/mybatis_cache.html 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用MyBatis的默认 ...
- Mybatis:缓存,动态SQL,注解SQL以及动态标签使用
1 转义字符 字符 转义 描述 < < 小于 <= <= 小于等于 > > 大于 >= >= 大于等于 <> <> 不等于 &a ...
- 使用MyBatis缓存
(1).为什么需要使用缓存:: MyBatis是一个持久层(数据库层)映射框架,在所有访问数据库的操作中,无疑数据查询是最耗费数据库资源的操作了,因为你一次可能需要查询成千上百万条记录(如果你不加限制 ...
- MyBatis缓存通俗易懂
1.1 mybatis缓存介绍 如下图,是mybatis一级缓存和二级缓存的区别图解: Mybatis一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的 ...
- mybatis缓存源码分析之浅谈缓存设计
本文是关于mybatis缓存模块设计的读后感,关于缓存的思考,关于mybatis的缓存源码详细分析在另一篇文章:https://www.cnblogs.com/gmt-hao/p/12448896.h ...
- mybatis缓存
mybatis缓存http://www.cnblogs.com/QQParadise/articles/5109633.htmlhttp://www.mamicode.com/info-detail- ...
- Mybatis缓存处理机制
一.MyBatis缓存介绍 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Se ...
- MyBatis缓存禁用失败
问题:MyBatis缓存无法禁用,同一个session的select查询结果一样,但是数据库其实已改变. 尝试达到想要的目的: 1.msgmapper.xml里的select标签加上 <sele ...
随机推荐
- JavaWeb--Cookie与Session
前言 Java Web 其实就是一个技术的总和,把Web看成一个容器而已主要使用JavaEE技术来实现.在加上各种中间件. 整个javaWeb阶段的内容通过实际的案例贯穿学习, 所涉及到的技术知识点会 ...
- SpringBoot定时任务 - Spring自带的定时任务是如何实现的?有何注意点?
通过前文我们基本梳理了定时任务体系:Timer和ScheduledExecutorService是JDK内置的定时任务方案,以及Netty内部基于时间轮实现的HashedWheelTimer,再到Qu ...
- 60行从零开始自己动手写FutureTask是什么体验?
前言 在并发编程当中我们最常见的需求就是启动一个线程执行一个函数去完成我们的需求,而在这种需求当中,我们常常需要函数有返回值.比如我们需要同一个非常大的数组当中数据的和,让每一个线程求某一个区间内部的 ...
- Eclipse里项目名有红叉,但是项目里的每一个文件都没有红叉
原因一:导入的文件被删除了. 解决方法:右击项目名,在弹出的菜单中,选择"Bulid Path"-->"configure build path"--&g ...
- Markdown 常用语法精讲
- #### 标题 (`# 跟标题名称一定要留空格`)> > # 一级标题> ## 二级标题> ### 三级标题> #### 四级标题> ##### 五级标题> ...
- 在Kubernetes上部署k6的详细步骤
k6介绍 k6是一款使用go语言编写的开源测试工具,支持用户编写测试脚本,解决了JMeter不易代码化的缺点.它的主要特点有 提供了友好的 CLI 工具 使用 JavaScript 代码编写测试用例 ...
- Taurus.MVC WebAPI 入门开发教程8:WebAPI文档与自动化测试。
系列目录 1.Taurus.MVC WebAPI 入门开发教程1:框架下载环境配置与运行. 2.Taurus.MVC WebAPI 入门开发教程2:添加控制器输出Hello World. 3.Tau ...
- 究竟什么是Shadow DOM?
shadow dom 是什么? 顾名思义,shadow dom直译的话就是影子dom,但我更愿把它理解为DOM中的DOM.因为他能够为Web组件中的 DOM和 CSS提供了封装,实际上是在浏览器渲染文 ...
- 如何通过C#/VB.NET设置Word文档段落缩进
缩进是指调整文本与页面边界之间的距离.在水平标尺,有四个段落缩进滑块:首行缩进.悬挂缩进.左缩进以及右缩进.在对于word文档的录入时,常常需要注意录入的格式,通过合理地设置段落格式,可以让文稿看起来 ...
- RabbitMQ 入门系列:9、扩展内容:死信队列:真不适合当延时队列。
系列目录 RabbitMQ 入门系列:1.MQ的应用场景的选择与RabbitMQ安装. RabbitMQ 入门系列:2.基础含义:链接.通道.队列.交换机. RabbitMQ 入门系列:3.基础含义: ...