Mybatis的缓存

2020-03-11  本文已影响0人  名字是乱打的

一 Mybatis缓存体系图

Mybatis缓存的基础实现是perpetualCache,但是mybatis利用装饰者模式对基础cache提供了许多的增强功能,比如上图,BlockingCache里利用concurrentHashMap封装了一些可重入锁Reetranlock实现了并发问题的解决

/**
 * Simple blocking decorator
 *
 * Simple and inefficient version of EhCache's BlockingCache decorator.
 * It sets a lock over a cache key when the element is not found in cache.
 * This way, other threads will wait until this element is filled instead of hitting the database.
 *
 * @author Eduardo Macarron
 *
 */
*当在缓存中找不到元素时,它设置对缓存键的锁定。
*这样,其他线程将等待此元素被填充,而不是命中数据库。
*锁acquire和release详情请看源码.

一 . 一级缓存的工作位置和维护对象

一级缓存的作用域是sqlsession,而且根据下图,查看一下sqlsession的实现类可以发现configuration是我们加载xml文件的全局变量,肯定不是sqlsess的工作位置,那么只有executor了

而且作为Executor 我们这里有simple reuser batch Executor三种,他们都继承了BaseExecutor
看下baseExecutor果然发现里面有个perpetualCache作为一级缓存,所以我们也称一级缓存为本地缓存,因为我们每连接一次数据库就会创建一个会话,每创建一个会员就会创建一个执行器,每个执行器里就有一个一级缓存.

我们用户去查询数据时候会先到一级缓存中尝试获取数据,如果有数据会直接返回不在查库,如果没找到数据会先返回应用再写入缓存,如下.
测试一级缓存

1、在同一个 session 中共享

BlogMapper mapper = session.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1)); 
System.out.println(mapper.selectBlog(1));

2、不同 session 不能共享

SqlSession session1 = sqlSessionFactory.openSession(); 
BlogMapper mapper1 = session1.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1));

PS:一级缓存在 BaseExecutor 的 query()——queryFromDatabase()中存入。在 queryFromDatabase()之前会 get()。


3、同一个会话中,update(包括 delete)增删改会导致一级缓存被清空

测试代码.
mapper.updateByPrimaryKey(blog); 
session.commit();
System.out.println(mapper.selectBlogById(1));

一级缓存是在 BaseExecutor 中的 update()方法中调用 clearLocalCache()清空的 (无条件),query 中会判断。
为什么呢?
如下图所示,我们的mapper元素属性中有个flushCache,在增删改里他是开启的true,在查询select里它是关闭的.这个会刷新该会话的缓存


4、其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)
//会话 2 更新了数据,会话 2 的一级缓存更新
BlogMapper mapper2 = session2.getMapper(BlogMapper.class); 
mapper2.updateByPrimaryKey(blog); 
session2.commit();
// 会话 1 读取到脏数据,因为一级缓存不能跨会话共享 System.out.println(mapper1.selectBlog(1));

一级缓存的不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据 可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要 解决这个问题,就要用到二级缓存。

如何关闭一级缓存呢?
方法:在配置文件的setings中更改localCacheScope属性值为STATEMENT
原理: 原理解释来自mybatis3官方文档

二 . 二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,\color{red}{范围是 namespace 级别 }的,其实也就是\color{red}{同一个接口}可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。

如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是 在一级缓存之后呢?二级缓存是在哪里维护的呢?

二级缓存应该是工作在一级缓存之前.(如果二级缓存中有就会直接返回,如果二级缓存没有,会去一级缓存中查,一级缓存也没有会去datasource中查,并依次存储,详情可以看后面有个流程图)

要跨会话共享的话,SqlSession 本 身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor 之外创建一个对象。实际上我们的二级缓存还是利用的装饰者模式做了一个包装类cachingExecutor对一级缓存做了增强,如果启用了 二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。

TransactionalCacheManager

CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接 返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行 查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户.

开启二级缓存的方法

第一步:在 mybatis-config.xml 中配置了(可以不配置,默认是 true):

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

只要没有显式地设置cacheEnabled=false,都会用 CachingExecutor 装饰基本的 执行器
第二步:在 Mapper.xml 中配置<cache/>标签:

<!-- 声明这个 namespace 使用二级缓存 --> 
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!—最多缓存对象个数,默认 1024-->
eviction="LRU" <!—回收,淘汰策略--> 
flushInterval="120000" <!—自动刷新时间 ms,多久自动刷新一次二级缓存,未配置时只有调用时刷新--> 
readOnly="false"/> <!—默认是 false(安全),改为 true 可读写时,对象必须支持序列化 -->

第三步:确保要使用缓存的select语句没有关闭缓存
我们mapper.xml文件里面的元素有个属性 usecache="",默认=ture,如果你添加了usecache="false",那么他就不会走缓存了
cache 属性详解:


Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert() 会刷新缓存。

我们配置二级缓存后,内部会通过一个CacheingExecutor对原来的Executor进行一个装饰,这样如果我们二级缓存中有数据就会直接返回,如果


二级缓存工作流程以及原理

思考:如果 cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗? 还会出现 CachingExecutor 包装对象吗?
会.

只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置<cache>,决定了在 启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query 方法里面的判断:
if (cache != null) { }

思考:如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true)
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">

思考:思考:为什么事务不提交,二级缓存不生效?

因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了 TransactionalCache 的getObject()、putObject和 commit()方法,
TransactionalCache 里面又持有了真正的 Cache 对象,比如是经过层层装饰的 PerpetualCache。
\color{red}{在 putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的}
\color{red}{ commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。}
\color{red}{它就是在 DefaultSqlSession 调用 commit()的时候被调用的}

思考:为什么增删改操作会清空缓存?

CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms)isFlushCacheRequired 就是从标签里面渠道的 flushCache的值。
而增删改操作的 flushCache 属性默认为 true。

三 . 什么时候开启二级缓存?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问 题,在什么情况下才有必要去开启二级缓存?

四 . 那么如何解决二级缓存的作用范围还是比较窄,会出现多个mapper之间的脏数据问题呢?
第三方缓存做二级缓存

除了 MyBatis 自带的二级缓存之外,我们也可以通过实现 Cache 接口来自定义二级 缓存。MyBatis 官方提供了一些第三方缓存集成方式,比如 ehcache 和 redis: https://github.com/mybatis/redis-cache pom 文件引入依赖
1.pom 文件引入依赖:

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

2.Mapper.xml 配置,type 使用 RedisCache:

<cache type="org.mybatis.caches.redis.RedisCache"
 eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

3.redis.properties 配置:

host=localhost 
port=6379 
connectionTimeout=5000 
soTimeout=5000 
database=0

当然,我们也可以使用独立的缓存服务,不使用 MyBatis 自带的二级缓存。


说到这里大部分人都会觉得真麻烦....真鸡肋...
实际上确实如此
生产中我们绝大多数时候还是直接用如redis的第三方缓存库,直接专门做的缓存.

上一篇下一篇

猜你喜欢

热点阅读