vlambda博客
学习文章列表

认识MyBatis 的缓存策略


MyBatis 缓存详解

Cache 缓存

缓存一般是 ORM框架 都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口

缓存体系结构

MyBatis 跟缓存相关的类都在Cache 包里面,其中有一个Cache 接口,只有一个默认的实现类 PerpetualCache ,它是用HashMap实现的。

除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等。

但是无论如何装饰,经过多少层的装饰,最后使用的还是基本的实现类(默认 PerpetualCache)


所有的缓存实现类上总体上可以分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

缓存实现类 描述 作用 装饰条件
基本缓存 缓存基本实现类 默认是PerpetualCache,也可以自定义比如RedisCache、EhCache等,具备基本功能的缓存
LruCache LRU策略的缓存 当缓存达到上限的时候,删除最近最少使用的缓存(Least Recently Use) evicition=“LRU”(默认)
FifoCache FIFO策略缓存 当缓存到达上限时候,删除最先入队的缓存 eviction="FIFO"
SoftCache WeakCache 常清理策略的缓存 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存,基于SoftReference和WeakReference eviction="SOFT" eviction="WEAK"
LoggingCache 常用日志功能的缓存 比如:输出缓存命中率 基本
Synchronized Cache 同步缓存 基于Synchronized 关键字实现,解决并发问题。 基本
BlockingCache 阻塞缓存 通过在get/put 方式中添加锁,保证只有一个线程操作缓存,基于java 重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化 readOnly=false(默认)
ScheduledCache 定时调度的缓存 在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时),如果是则清空缓存--即每个一段时间清空一次缓存 flushInterval 不为空
TransactionalCache 事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 在Transaction CacheManager 中用Map 维护对应关系

那问题来了?缓存是在什么时候被创建,又是在什么情况下被装饰的呢?

一级缓存

一级缓存(本地缓存)介绍

一级缓存也叫本地缓存,MyBatis 的一级缓存 是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存默认是开启的,不需要任何的配置。

首先我们要弄明白,在MyBatis 执行的流程里面,涉及到这么多的对象,缓存PerpetualCache 应该放在哪个对象里面进行维护?如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性。

DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在Executor 里面维护--SimpleExecutor/ReuseExecutor/BatchExecutor 的父类 BaseExecutor 的构造函数中已经持有了PerpetualCache

在同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再发送SQL到数据库。但是不同的会话里面,即使执行的SQL一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。

认识MyBatis 的缓存策略

一级缓存的验证

判断是否命中缓存:如果再次发送SQL到数据库执行,说明没有命中缓存,如果直接打印对象,说明是从内存缓存中取到了结果。

1、在同一个 session 中共享

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

2、不同session 不能共享

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

一级缓存在BaseExecutor 的query() -- queryFormDataBase() 中存入,在queryFromDataBase() 之前会get()。

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

mapper.upateByPrimaryKey(blog);
session.commit();

System.out.println(mapper.selectBlogById(1));

一级缓存是在BaseExecutor 中的 update() 方法中调用 clearLocalCache() 清空的,query 中会判断

4、如果跨会话,会发生什么问题?其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)

//会话2 更新了数据,会话2 的一级缓存更新
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(Blog);
session2.commit();

//会话1 读取到脏数据,因为一级缓存不能跨会话共享
System.out.println(mapper1.selectBlog(1));

一级缓存的不足

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

二级缓存

二级缓存的介绍

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

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

作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享,而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只要读取不到二级缓存的情况下才会到一个会话中去取一级缓存。

思考下:二级缓存在哪个对象中进行维护的呢?要跨会话共享的话,SqlSession 本身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外 创建一个对象。

实际上MyBatis 用了 装饰器的类来维护,就是CacheingExecutor 。如果启用了 二级缓存,MyBatis 在创建 Executor 对象的时候会对Executor 进行装饰。

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

开启二级缓存的方法

1、在MyBatis-config.xml 中配置(也可以不配置,默认是true)

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

只要没有显示地设置cacheEnabled = false ,都会用 CachingExecutor 装饰基本的执行器

2、在Mapper.xml 中配置 <cache/> 标签:

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

Cache属性详解

属性 含义 取值
type 缓存实现类 需要实现cahce接口,默认是PerpetualCache
size 最多缓存对象的个数 默1024
eviction 回收策略(缓存淘汰策略) LRU  -   最近最少使用的:移除长时间不被使用的对象(默认)。FIFO -  先进先出:按照对象进入缓存的顺序来移除他们。SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK - 弱引用:更积地移除基于垃圾收集器状和弱引用规则的对象。
flushInterval 定时自动清空缓存间隔 自动刷新时间,单位ms,未配置 时只有调用时刷新
readOnly 是否只读 true:只读缓存:会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改,这提供了很重要的性能优势。false:读写缓存:会返回缓存对象的拷贝(通过序列化),不会共享,这会慢一些,但是安全,因为默认是false。改为false可读写时,对象必须支持序列化。
blocking 是否使用可重入锁实现缓存的并发控制 true,会使用BlockingCache 对Cache 进行装饰默认false

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

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

只要CacheEnabled = true 基本执行器 就会被装饰,有没有配置 <cache> ,决定了 在启动的时候 会不会创建这个mapper 的cache 对象,最终会影响到CachingExecutorquery 方法里面的判断:

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

我们可以在单个 Statement ID 上显示关闭二级缓存(默认是true):

<select id="selectBlog" resultMap="BaseResultMap" useCache="fasle"></select>

二级缓存的验证

1、事务不提交,二级缓存不生效

BlogMapper mapper1 = session.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
//事务不提交的情况下,二级缓存不会写入
//session1.commit()
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));

因为二级缓存使用TransactionalCacheManager(TCM) 来进行管理,最后又调用了 TransactionalCache 的getObject()、putObject()、commit() 方法,TransactionalCache 里面持又持有了真正的Cache 对象,比如是经过层层装饰的PrepetualCache。

在putObject 的时候,只是添加到了 entriesToAddOnCommit 里面,只有它的commit() 方法被调用的时候才会调用 flushPendingEntries() 真正写入缓存,他就是在DefaultSqlSession 调用 commit() 的时候被调用的。

2、使用不同的session 的 mapper ,验证 二级缓存 可以跨 session 存在,取消以上 commit()的注释。

3、在其他的session 中执行增删改操作的时候,验证缓存会被刷新

Blog blog = new Blog();
blog.setBid(1);
blog.setName("357");
mapper3.updateByPrimaryKey(blog);
session3.commit();
//执行了更新操作,二级缓存失效,再次发送SQL查询
System.out.println(mapper2.selectBlogById);

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

什么时候开启二级缓存呢?

一级缓存 默认是打开的,二级缓存需要配置 才可以开启,那么什么时候才有必要去开启二级缓存呢?

1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所有适合在查询为主的应用中使用,比如历史交易,历史订单查询。否则缓存就失去了意义。

2、如果多个namespace 中有针对 同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现 读到脏数据 的情况。所以,推荐在一个Mapper 里面只操作 单表的情况 使用。

那么我们如何让多个namespace 共享一个二级缓存呢?

跨namespace 的缓存共享问题,可以使用<cache-ref> 来解决:

<cache-ref namespace="com.gupaoedu.curd.dao.DepartmentMapper"></cache-ref>

cache-ref 代表引用别的命名空间 的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者业务可以对表进行分组的时候可以使用。

注意:这种情况下,多个Mapper 的操作都会引起缓存刷新,缓存的意义已经不大了。

第三方缓存做二级缓存

除了MyBatis 自带的 二级缓存以外,我们也可以通过实现Cache 接口来自定义 二级缓存。

MyBatis 官方提供了一些第三方缓存集成方式,比如ehcache 和 redis;

pom文件引入:

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

Mapper.xml 配置 ,type 使用 RedisCache:

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

redis.properties 的配置

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

Redis 作为二级缓存的验证

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