共计 8624 个字符,预计需要花费 22 分钟才能阅读完成。
前言
在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如 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 | 缓存默认实现类 | – | 基本功能,默认携带 |
LruCache | LRU 淘汰策略缓存(默认淘汰策略) | 当缓存达到下限,删除最近起码应用缓存 | eviction=“LRU” |
FifoCache | FIFO 淘汰策略缓存 | 当缓存达到下限,删除最先入队的缓存 | eviction=“FIFO” |
SoftCache | JVM 软援用淘汰策略缓存 | 基于 JVM 的 SoftReference 对象 | eviction=“SOFT” |
WeakCache | JVM 弱援用淘汰策略缓存 | 基于 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=localhost
port=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 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!