一 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官方文档
二 . 二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,的,其实也就是,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。
如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是 在一级缓存之后呢?二级缓存是在哪里维护的呢?
二级缓存应该是工作在一级缓存之前.(如果二级缓存中有就会直接返回,如果二级缓存没有,会去一级缓存中查,一级缓存也没有会去datasource中查,并依次存储,详情可以看后面有个流程图)
要跨会话共享的话,SqlSession 本 身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor 之外创建一个对象。实际上我们的二级缓存还是利用的装饰者模式做了一个包装类cachingExecutor对一级缓存做了增强,如果启用了 二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。
TransactionalCacheManagerCachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接 返回,如果没有委派交给真正的查询器 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。
思考:为什么增删改操作会清空缓存?
在 CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms),
isFlushCacheRequired
就是从标签
里面渠道的flushCache
的值。
而增删改操作的 flushCache 属性默认为 true。
三 . 什么时候开启二级缓存?
一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问 题,在什么情况下才有必要去开启二级缓存?
-
1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以少在增删改为主的语句中使用,,比如历史交易、历史订单的查询。否则缓存就失去了意义。
-
2、如果多个 namespace 中有针对于同一个表的操作,比如 Blog 表,如果在一个 namespace 中刷新了缓存,另一个 namespace 中没有刷新,使用一级缓存就会出现读到脏数据的情况。所以,推荐在一个 Mapper 里面只操作单表的情况使用。,
如果是多个mapper共同协作那么势必会出现不同namespace,这就使得我们的二级缓存不能跨namespace存取数据,可能会出现另一种如一级缓存的脏数据问题.
四 . 那么如何解决二级缓存的作用范围还是比较窄,会出现多个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的第三方缓存库,直接专门做的缓存.
网友评论