热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

五分钟,带你彻底掌握MyBatis缓存的工作原理

点击上方“芋道源码”,选择“设为星标”管她前浪,还是后浪?能浪的浪,才是好浪!每天8:55更新文章࿰

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 8:55 更新文章,每天掉亿点点头发...

源码精品专栏

 
  • 原创 | Java 2020 超神之路,很肝~

  • 中文详细注释的开源项目

  • RPC 框架 Dubbo 源码解析

  • 网络应用框架 Netty 源码解析

  • 消息中间件 RocketMQ 源码解析

  • 数据库中间件 Sharding-JDBC 和 MyCAT 源码解析

  • 作业调度中间件 Elastic-Job 源码解析

  • 分布式事务中间件 TCC-Transaction 源码解析

  • Eureka 和 Hystrix 源码解析

  • Java 并发源码

来源:blog.csdn.net/zwx900102/article/details/108696005

  • 前言

  • 为什么要缓存

  • MyBatis缓存

    • 一级缓存

    • 二级缓存

    • 二级缓存应该开启吗

    • 自定义缓存

  • 总结


前言

在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如Redis也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存。自然的,作为一款优秀的ORM框架,MyBatis中又岂能少得了缓存,那么本文的目的就是带领大家一起探究一下MyBatis的缓存是如何实现的。给我五分钟,带你彻底掌握MyBatis的缓存工作原理

为什么要缓存

在计算机的世界中,CPU的处理速度可谓是一马当先,远远甩开了其他操作,尤其是I/O操作,除了那种CPU密集型的系统,其余大部分的业务系统性能瓶颈最后或多或少都会出现在I/O操作上,所以为了减少磁盘的I/O次数,那么缓存是必不可少的,通过缓存的使用我们可以大大减少I/O操作次数,从而在一定程度上弥补了I/O操作和CPU处理速度之间的鸿沟。而在我们ORM框架中引入缓存的目的就是为了减少读取数据库的次数,从而提升查询的效率。

MyBatis缓存

MyBatis中的缓存相关类都在cache包下面,而且定义了一个顶级接口Cache,默认只有一个实现类PerpetualCache,PerpetualCache中是内部维护了一个HashMap来实现缓存。下图就是MyBatis中缓存相关类:需要注意的是decorators包下面的所有类也实现了Cache接口,那么为什么我还是要说Cache只有一个实现类呢?其实看名字就知道了,这个包里面全部是装饰器,也就是说这其实是装饰器模式的一种实现。

我们随意打开一个装饰器:可以看到,最终都是调用了delegate来实现,只是将部分功能做了增强,其本身都需要依赖Cache的唯一实现类PerpetualCache(因为装饰器内需要传入Cache对象,故而只能传入PerpetualCache对象,因为接口是无法直接new出来传进去的)

在MyBatis中存在两种缓存,即一级缓存二级缓存

一级缓存

一级缓存也叫本地缓存,在MyBatis中,一级缓存是在会话(SqlSession)层面实现的,这就说明一级缓存作用范围只能在同一个SqlSession中,跨SqlSession是无效的。

MyBatis中一级缓存是默认开启的,不需要任何配置。我们先来看一个例子验证一下一级缓存是不是真的存在,作用范围又是不是真的只是对同一个SqlSession有效。

一级缓存真的存在吗

package com.lonelyWolf.mybatis;import com.lonelyWolf.mybatis.mapper.UserAddressMapper;
import com.lonelyWolf.mybatis.mapper.UserMapper;
import com.lonelyWolf.mybatis.model.LwUser;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;
import java.io.InputStream;
import java.util.List;public class TestMyBatisCache {public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml";//读取mybatis-config配置文件InputStream inputStream = Resources.getResourceAsStream(resource);//创建SqlSessionFactory对象SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//创建SqlSession对象SqlSession session = sqlSessionFactory.openSession();UserMapper userMapper = session.getMapper(UserMapper.class);List userList =  userMapper.selectUserAndJob();List userList2 =  userMapper.selectUserAndJob();}
}

执行后,输出结果如下:我们可以看到,sql语句只打印了一次,这就说明第2次用到了缓存,这也足以证明一级缓存确实是存在的而且默认就是是开启的。

一级缓存作用范围

现在我们再来验证一下一级缓存是否真的只对同一个SqlSession有效,我们对上面的示例代码进行如下改变:

 SqlSession session1 = sqlSessionFactory.openSession();SqlSession session2 = sqlSessionFactory.openSession();UserMapper userMapper1 = session1.getMapper(UserMapper.class);UserMapper userMapper2 = session2.getMapper(UserMapper.class);List userList =  userMapper1.selectUserAndJob();List userList2 =  userMapper2.selectUserAndJob();

这时候再次运行,输出结果如下:可以看到,打印了2次,没有用到缓存,也就是不同SqlSession中不能共享一级缓存。

一级缓存原理分析

首先让我们来想一想,既然一级缓存的作用域只对同一个SqlSession有效,那么一级缓存应该存储在哪里比较合适是呢?

是的,自然是存储在SqlSession内是最合适的,那我们来看看SqlSession的唯一实现类DefaultSqlSession:DefaultSqlSession中只有5个成员属性,后面3个不用说,肯定不可能用来存储缓存,然后Configuration又是一个全局的配置文件,也不合适存储一级缓存,这么看来就只有Executor比较合适了,因为我们知道,SqlSession只提供对外接口,实际执行sql的就是Executor。

既然这样,那我们就进去看看Executor的实现类BaseExecutor:看到果然有一个localCache。而上面我们有提到PerpetualCache内缓存是用一个HashMap来存储缓存的,那么接下来大家肯定就有以下问题:

  • 缓存是什么时候创建的?

  • 缓存的key是怎么定义的?

  • 缓存在何时使用

  • 缓存在什么时候会失效?

接下来就让我们逐一分析

一级缓存CacheKey的构成

既然缓存那么肯定是针对的查询语句,一级缓存的创建就是在BaseExecutor中的query方法内创建的:createCacheKey这个方法的代码就不贴了,在这里我总结了一下CacheKey的组成,CacheKey主要是由以下6部分组成

  • 1、将Statement中的id添加到CacheKey对象中的updateList属性

  • 2、将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0)

  • 3、将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE)

  • 4、将sql语句(包括占位符?)添加到CacheKey对象中的updateList属性

  • 5、循环用户传入的参数,并将每个参数添加到CacheKey对象中的updateList属性

  • 6、如果有配置Environment,则将Environment中的id添加到CacheKey对象中的updateList属性

一级缓存的使用

创建完CacheKey之后,我们继续进入query方法:可以看到,在查询之前就会去localCache中根据CacheKey对象来获取缓存,获取不到才会调用后面的queryFromDatabase方法

一级缓存的创建

queryFromDatabase方法中会将查询得到的结果存储到localCache中

一级缓存什么时候会被清除

一级缓存的清除主要有以下两个地方:

  • 1、就是获取缓存之前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的创建代码截图),如果配置了则会清除一级缓存。

  • 2、MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。

  • 3、在执行commit,rollback,update方法时会清空一级缓存。

PS:利用插件我们也可以自己去将缓存清除,后面我们会介绍插件相关知识。

二级缓存

一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。

二级缓存的作用范围

一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的就是要实现作用范围更广,那肯定是要实现跨会话共享的,在MyBatis中二级缓存的作用域是namespace,也就是作用范围是同一个命名空间,所以很显然二级缓存是需要存储在SqlSession之外的,那么二级缓存应该存储在哪里合适呢?

在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,这就是我们上一篇文章介绍Executor时还留下的没有介绍的一个对象:CachingExecutor。

如何开启二级缓存

二级缓存相关的配置有三个地方:1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。


1

想详细了解mybatis-config的可以点击这里。2、在Mapper映射文件内需要配置缓存标签:




想详细了解Mapper映射的所有标签属性配置可以点击这里。3、在select查询语句标签上配置useCache属性,如下:

select * from lw_user

以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。

不过开启二级缓存的时候有两点需要注意:1、需要commit事务之后才会生效 2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)

如果不实现序列化接口则会报如下错误:接下来我们通过一个例子来验证一下二级缓存的存在,还是用上面一级缓存的例子进行如下改造:

 SqlSession session1 = sqlSessionFactory.openSession();UserMapper userMapper1 = session1.getMapper(UserMapper.class);List userList =  userMapper1.selectUserAndJob();session1.commit();//注意这里需要commit,否则缓存不会生效SqlSession session2 = sqlSessionFactory.openSession();UserMapper userMapper2 = session2.getMapper(UserMapper.class);List userList2 =  userMapper2.selectUserAndJob();

然后UserMapper.xml映射文件中,新增如下配置:


运行代码,输出如下结果:上面输出结果中只输出了一次sql,说明用到了缓存,而因为我们是跨会话的,所以肯定就是二级缓存生效了。

二级缓存原理分析

上面我们提到二级缓存是通过CachingExecutor对象来实现的,那么就让我们先来看看这个对象:我们看到CachingExecutor中只有2个属性,第1个属性不用说了,因为CachingExecutor本身就是Executor的包装器,所以属性TransactionalCacheManager肯定就是用来管理二级缓存的,我们再进去看看TransactionalCacheManager对象是如何管理缓存的:TransactionalCacheManager内部非常简单,也是维护了一个HashMap来存储缓存。HashMap中的value是一个TransactionalCache对象,继承了Cache。注意上面有一个属性是临时存储二级缓存的,为什么要有这个属性,我们下面会解释。

二级缓存的创建和使用

我们在读取mybatis-config全局配置文件的时候会根据我们配置的Executor类型来创建对应的三种Executor中的一种,然后如果我们开启了二级缓存之后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对我们的三种基本Executor进行包装,即使Mapper.xml映射文件没有开启也会进行包装。

接下来我们看看CachingExecutor中的query方法:上面方法大致经过如下流程:

  • 1、创建一级缓存的CacheKey

  • 2、获取二级缓存

  • 3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。

  • 4、查询到结果之后将结果进行缓存。

需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。所以我们看一下commit方法:

二级缓存如何进行包装

最开始我们提到了一些缓存的包装类,这些都到底有什么用呢?在回答这个问题之前,我们先断点一下看看获取到的二级缓存长啥样:从上面可以看到,经过了层层包装,从内到外一次经过如下包装:

  • 1、PerpetualCache:第一层缓存,这个是缓存的唯一实现类,肯定需要。

  • 2、LruCache:二级缓存淘汰机制之一。因为我们配置的默认机制,而默认就是LRU算法淘汰机制。淘汰机制总共有4中,我们可以自己进行手动配置。

  • 3、SerializedCache:序列化缓存。这就是为什么开启了默认二级缓存我们的结果集对象需要实现序列化接口。

  • 4、LoggingCache:日志缓存。

  • 5、SynchronizedCache:同步缓存机制。这个是为了保证多线程机制下的线程安全性。

下面就是MyBatis中所有缓存的包装汇总:

缓存包装器描述作用装饰条件
PerpetualCache缓存默认实现类-基本功能,默认携带
LruCacheLRU淘汰策略缓存(默认淘汰策略)当缓存达到上限,删除最近最少使用缓存eviction=“LRU”
FifoCacheFIFO淘汰策略缓存当缓存达到上限,删除最先入队的缓存eviction=“FIFO”
SoftCacheJVM软引用淘汰策略缓存基于JVM的SoftReference对象eviction=“SOFT”
WeakCacheJVM弱引用淘汰策略缓存基于JVM的WeakReference对象eviction=“WEAK”
LoggingCache带日志功能缓存输出缓存相关日志信息基本功能,默认包装
SynchronizedCache同步缓存基于synchronized关键字实现,用来解决并发问题基本功能,默认包装
BlockingCache阻塞缓存get/put操作时会加锁,防止并发,基于Java重入锁实现blocking=true
SerializedCache支持序列化的缓存通过序列化和反序列化来存储和读取缓存readOnly=false(默认)
ScheduledCache定时调度缓存操作缓存时如果缓存已经达到了设置的最长缓存时间时会移除缓存flushInterval属性不为空
TransactionalCache事务缓存在TransactionalCacheManager中用于维护缓存map的value值-

二级缓存应该开启吗

既然一级缓存默认是开启的,而二级缓存是需要我们手动开启的,那么我们什么时候应该开启二级缓存呢?

1、因为所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。

2、因为二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。

自定义缓存

一级缓存可能存在脏读情况,那么二级缓存是否也可能存在呢?

是的,默认的二级缓存毕竟也是存储在本地缓存,所以对于微服务下是可能出现脏读的情况的,所以这时候我们可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。

MyBatis官方提供的第三方缓存

MyBatis官方也提供了一些第三方缓存的支持,如:encache和redis。下面我们以redis为例来演示一下:引入pom文件:

org.mybatis.cachesmybatis-redis1.0.0-beta2

然后缓存配置如下:


然后在默认的resource路径下新建一个redis.properties文件:

host=localhost
port=6379
12

然后执行上面的示例,查看Cache,已经被Redis包装:

自己实现二级缓存

如果要实现一个自己的缓存的话,那么我们只需要新建一个类实现Cache接口就好了,然后重写其中的方法,如下:

package com.lonelyWolf.mybatis.cache;import org.apache.ibatis.cache.Cache;public class MyCache implements Cache {@Overridepublic String getId() {return null;}@Overridepublic void putObject(Object o, Object o1) {}@Overridepublic Object getObject(Object o) {return null;}@Overridepublic Object removeObject(Object o) {return null;}@Overridepublic void clear() {}@Overridepublic int getSize() {return 0;}
}

上面自定义的缓存中,我们只需要在对应方法,如putObject方法,我们把缓存存到我们想存的地方就行了,方法全部重写之后,然后配置的时候type配上我们自己的类就可以实现了,在这里我们就不做演示了

总结

本文主要分析了MyBatis的缓存是如何实现的,并且分别演示了一级缓存和二级缓存,并分析了一级缓存和二级缓存所存在的问题,最后也介绍了如何使用第三方缓存和如何自定义我们自己的缓存,通过本文,我想大家应该可以彻底掌握MyBatis的缓存工作原理了。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

兄弟,一口,点个!????


推荐阅读
  • Hibernate延迟加载深入分析-集合属性的延迟加载策略
    本文深入分析了Hibernate延迟加载的机制,特别是集合属性的延迟加载策略。通过延迟加载,可以降低系统的内存开销,提高Hibernate的运行性能。对于集合属性,推荐使用延迟加载策略,即在系统需要使用集合属性时才从数据库装载关联的数据,避免一次加载所有集合属性导致性能下降。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • GreenDAO快速入门
    前言之前在自己做项目的时候,用到了GreenDAO数据库,其实对于数据库辅助工具库从OrmLite,到litePal再到GreenDAO,总是在不停的切换,但是没有真正去了解他们的 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • struts2重点——ValueStack和OGNL
    一、值栈(ValueStack)1.实现类:OGNLValueStack2.对象栈:CompoundRoot( ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文整理了Java中io.netty.handler.codec.http.FullHttpRequest.content()方法的一些代码示例,展示了Fu ... [详细]
  • 详解Netty Zero Copy机制
    NettyZeroCopy的巧妙设计让Netty从众多高性 ... [详细]
  • 前言最近一段时间在整公司项目里一个功能的优化,用到了多线程处理。期间也是踩了不少的坑,在这里想说下我遇到的问题和注意事项。以及怎样知道启动的那些多线程都 ... [详细]
  • 吃透Netty源码系列四之NioEventLoop
    吃透Netty源码系列四之NioEventLoop新启动的线程的作用执行NioEventLoop的run方法执行任务一(通道注册register0)doRegisterpipeli ... [详细]
  • 阿里首席架构师科普RPC框架
    RPC概念及分类RPC全称为RemoteProcedureCall,翻译过来为“远程过程调用”。目前,主流的平台中都支持各种远程调用技术,以满足分布式系统架构中不同的系统之间的远程 ... [详细]
author-avatar
沈丶小浪_171
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有