关于java:5-分钟彻底掌握-MyBatis-缓存

44次阅读

共计 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 开发手册(嵩山版)》最新公布,速速下载!

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

正文完
 0