前言

在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如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<LwUser> userList =  userMapper.selectUserAndJob();        List<LwUser> 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<LwUser> userList =  userMapper1.selectUserAndJob(); List<LwUser> 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中为了实现二级缓存,专门用了一个装璜器来保护,这就是:CachingExecutor。

如何开启二级缓存

二级缓存相干的配置有三个中央:

1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。

<setting name="cacheEnabled" value="true"/>

想具体理解mybatis-config的能够点击这里。
2、在Mapper映射文件内须要配置缓存标签:

<cache/>或<cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>

想具体理解Mapper映射的所有标签属性配置能够点击这里。
3、在select查问语句标签上配置useCache属性,如下:

<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true">    select * from lw_user</select>

以上配置第1点是默认开启的,也就是说咱们只有配置第2点就能够关上二级缓存了,而第3点是当咱们须要针对某一条语句来配置二级缓存时候则能够应用。

不过开启二级缓存的时候有两点须要留神:

1、须要commit事务之后才会失效

2、如果应用的是默认缓存,那么后果集对象须要实现序列化接口(Serializable)

如果不实现序列化接口则会报如下谬误:

接下来咱们通过一个例子来验证一下二级缓存的存在,还是用下面一级缓存的例子进行如下革新:

SqlSession session1 = sqlSessionFactory.openSession();UserMapper userMapper1 = session1.getMapper(UserMapper.class);List<LwUser> userList =  userMapper1.selectUserAndJob();session1.commit();//留神这里须要commit,否则缓存不会失效SqlSession session2 = sqlSessionFactory.openSession();UserMapper userMapper2 = session2.getMapper(UserMapper.class);List<LwUser> userList2 =  userMapper2.selectUserAndJob();

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

<cache/>

运行代码,输入如下后果:

下面输入后果中只输入了一次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文件:

<dependency>    <groupId>org.mybatis.caches</groupId>    <artifactId>mybatis-redis</artifactId>    <version>1.0.0-beta2</version></dependency>

而后缓存配置如下:

<cache type="org.mybatis.caches.redis.RedisCache"></cache>

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

host=localhostport=6379

而后执行下面的示例,查看Cache,曾经被Redis包装:

本人实现二级缓存

如果要实现一个本人的缓存的话,那么咱们只须要新建一个类实现Cache接口就好了,而后重写其中的办法,如下:

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

下面自定义的缓存中,咱们只须要在对应办法,如putObject办法,咱们把缓存存到咱们想存的中央就行了,办法全副重写之后,而后配置的时候type配上咱们本人的类就能够实现了,在这里咱们就不做演示了

总结

本文次要剖析了MyBatis的缓存是如何实现的,并且别离演示了一级缓存和二级缓存,并剖析了一级缓存和二级缓存所存在的问题,最初也介绍了如何应用第三方缓存和如何自定义咱们本人的缓存,通过本文,我想大家应该能够彻底把握MyBatis的缓存工作原理了。

作者:双子孤狼\
链接:https://blog.csdn.net/zwx9001...
版权申明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协定,转载请附上原文出处链接和本申明。

近期热文举荐:

1.600+ 道 Java面试题及答案整顿(2021最新版)

2.终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞+转发哦!