前言
Mybatis 使用到了两种缓存:一级缓存(即本地缓存 local cache)和二级缓存(second level cache)。
一级缓存(local cache), 即本地缓存, 作用域默认为sqlSession。当Session flush或close 后,该Session中的所有Cache将被清空。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在当前SqlSession的一个基于PerpetualCache 类Map中,key:hashCode+查询的SqlId+编写的sql查询语句+参数。所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。
默认情况下,本地缓存的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存,达到让本地缓存失效的作用。如果你仅想让某个查询的本地缓存失效可以在select标签中添加flushCache=“true”,每次查询后不会将结果放入本地缓存。
localCacheScope=SESSION时,缓存对整个SqlSession有效,只有执行DML语句更新语句时,缓存才会被清除。
localCacheScope=STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
以下为具体示范:
@Transactional
public GradeParam selectByNameAndLevel(String name, GradeLevel level) {
// 测试一级缓存
gradeParamMapper.selectByNameAndLevel(name, level);
GradeParam gradeParam = gradeParamMapper.selectByNameAndLevel(name, level);
return gradeParam;
}
<select id="selectByNameAndLevel" resultType="GradeParam">
select
<include refid="columns" />
from grade_param
where name = #{param1,jdbcType=VARCHAR} and level = #{param2,jdbcType=TINYINT}
</select>
结果:
本例子基于spring boot项目构建,sqlSession生命周期交给了Spring管理,在开启事务的情况下,一个事务中共用同一个sqlSession。由测试结果可见,查询1过后,查询2不会再去查库,本地缓存起到了作用。查询出的两个对象都是同一个对象。
如果localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。
由于本地缓存作用于sqlSession生命周期,这样在session1中执行查询1过后,未执行查询2之前,执行session2的更新,更新了session1查询1查出来的数据,再执行session1的查询,仍旧是查询出查询1的旧数据,这样导致本地缓存无法获取最新的记录。解决方案:使用二级缓存。
综上两点,建议禁用一级缓存,如果需要缓存,可以考虑下文提到的二级缓存以及外部缓存。
(未 close SqlSession情况下)
不同的SqlSession对应不同的一级缓存
同一个SqlSession但是查询条件不同
同一个SqlSession两次查询期间执行了任何一次增删改操作
同一个SqlSession两次查询期间手动清空了缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享,二级缓存存在于 SqlSessionFactory生命周期中。如果你的MyBatis使用了二级缓存,并且你的Mapper中select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。通过当前查询,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
CachingExecutor 对于查询请求处理,CachingExecutor中query方法获取MappedStatement的cache缓存,封装到TransactionalCacheManager,通过TransactionalCacheManager、cacheKey未提交先将结果集存在集合中,commit时来操作存入cache。
CachingExecutor 中的核心源码
略
全局开关
在 mybatis 中, 二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml 中如下配置:
<settings>
<!--全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 -->
<setting name="cacheEnabled" value="true"/>
</settings>
分开关
配置完全局开关还需配置分开关才能使二级缓存起作用。在mapper.xml中配置分开关具体配置如下:
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false"/>
cache标签可以配置的属性:
entity 实现序列化接口
/**
* 年级参数类
*
* @author : Nestor.Bian
* @version : V 1.0
* @date : 2020/3/20
*/
@Data
public class GradeParam implements Serializable {
public static final long serialVersionUID = 1L;
private Long id;
private String name;
private String managerName;
@JsonDeserialize(using = BaseEnumJsonDeserializer.class)
private GradeLevel level;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
必不可少的步骤:存取对象到二级缓存中需要序列化和反序列化。
示例
GradeParamMapper.xml中添加分开关配置
<cache eviction="LRU" blocking="false" flushInterval="180000" size="2048" readOnly="fals/>
@Transactional
public GradeParam selectByNameAndLevel(String name, GradeLevel level) {
GradeParam gradeParam = gradeParamMapper.selectByNameAndLevel(name, level);
return gradeParam;
}
第一次api接口调用:
SqlSession 查询出年级参数, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是没有命中。 sqlSession.close()后, 此时将数据序列化并保持到二级缓存中。
第二次api接口调用:
查询出年级参数对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句,命中率 0.5
由于 readOnly=“false”, 由于都是反序列化得到的, 两次查询出的对象为不同的实例。
注意点
由于在更新时会刷新缓存, 因此需要注意使用场合:查询频率很高, 更新频率很低时使用, 即经常使用 select, 相对较少使用delete, insert, update。
缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响。但刷新缓存是刷新整个 namespace 的缓存, 也就是你 update 了一个, 则整个namespace 缓存都被清空。可以使用,让多个mapper.xml使用同一个namespace,这样一个mapper.xml中的操作刷新了缓存,那么整个namespace下的缓存都会被清除。
最好在 「只有单表操作」 的表的 namespace 使用缓存, 而且对该表的操作都在这个 namespace 中。 否则可能会出现脏读的情况。
引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。
集成EhCache缓存
缓存数据有内存和磁盘两级,无须担心容量问题。
缓存数据会在虚拟机重启的过程中写入磁盘。可以通过RMI、可插入API等方式进行分布式缓存。
具有缓存和缓存管理器的侦昕接口。
支持多缓存管理器实例以及一个实例的多个缓存区域。
添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
配置 EhCache
在 src/main/resources 目录下新增 ehcache.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd"
updateCheck="false" monitoring="autodetect"
dynamicConfig="true">
<!--指定数据在磁盘中的存储位置-->
<diskStore path="D:/ehcache"/>
<defaultCache
maxElementsInMemory="10000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
diskPersistent="true">
</defaultCache>
<cache name="com.nestor.mybatisdemo.mapper.SchoolMapper"
maxElementsInMemory="3000"
eternal="false"
copyOnRead="true"
copyOnWrite="true"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
</ehcache>
diskStore:指定数据在磁盘中的存储位置。
defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用指定的的管理策略。
以下属性是必须的:
maxElementsInMemory - 在内存中缓存的element的最大数目。不存放在内存中需要设置成1,Ehcache 2.0后设置成0表示不限制最大数目
maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
overflowToDisk - 设定当内存缓存溢出的时候是否将element缓存到磁盘上 。
以下属性是可选的:
timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB - 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区。
diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作。
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候,移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)。
copyOnRead - 判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true ,那就是可读写缓存,每次读取缓存时都会复制一个新的实例 。
copyOnWrite - 判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使用只读缓存,可以不配置这两个属性,使用默认值false 即可 。
ehcache-cache 提供了如下 2 个可选的缓存实现。
org.mybatis.caches.ehcache.EhcacheCache
org.mybatis.caches.ehcache.LoggingEhcache 这个是带日志的缓存。
在Mapper.xml中添加如下配置即可
<mapper namespace="com.nestor.mybatisdemo.mapper.GradeParamMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
</mapper>
只通过设置 type属性就可以使用 EhCache 缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,命名空间没有特殊配置默认使用默认的缓存配置。如果想针对某一个命名空间进行配置,需要在 ehcache.xml 中添加一个和映射文件命名空间一致的缓存配置,例如针对SchoolMapper可以进行如下配置。
<cache name="com.nestor.mybatisdemo.mapper.SchoolMapper"
maxElementsInMemory="3000"
eternal="false"
copyOnRead="true"
copyOnWrite="true"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
配置Redis缓存
添加依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
在mapper.xml中添加如下配置即可。
<mapper namespace="com.nestor.mybatisdemo.mapper.GradeParamMapper">
<cache type="org.mybatis.caches.redis.RedisCache" />
</mapper>
配置依然很简单, RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。因为Redis作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis 的缓存并不会因为应用的关闭而失效。当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCache缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存 ,这种方式会消耗更多的服务器资源。如果使用类似 Redis 的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享 。