Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码
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"
,每次查询后不会将结果放入本地缓存。
<setting name="localCacheScope" value="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 所返回的对象作出更改,以防后患。综上两点,建议禁用一级缓存,如果需要缓存,可以考虑下文提到的二级缓存以及外部缓存。
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是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 中的核心源码
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public void commit(boolean required) throws SQLException {
this.delegate.commit(required);
this.tcm.commit();
}
在 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标签可以配置的属性:
属性 | 描述 |
---|---|
eviction | 缓存的回收策略:LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。 |
flushInterval | 缓存刷新间隔,缓存多长时间清空一次,默认不清空,设置一个毫秒值 |
readOnly | 是否只读:true:只读,mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快。false:非只读,mybatis觉得获取的数据可能会被修改。mybatis会利用序列化和反序列的技术克隆一份新的数据给你。安全,速度慢。默认是true。 |
size | 对应为引用的数量,即最多的缓存对象数据, 默认为 1024。 |
type | 指定自定义缓存的全类名,mybatis 默认缓存实现类 org.apache.ibatis.cache.impl.PerpetualCache。实现Cache接口即可 |
blocking | blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache 进行封装。使用 BlockingCache 会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。 |
/**
* 年级参数类
*
* @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”, 由于都是反序列化得到的, 两次查询出的对象为不同的实例。
,让多个mapper.xml使用同一个namespace,这样一个mapper.xml中的操作刷新了缓存,那么整个namespace下的缓存都会被清除。缓存数据有内存和磁盘两级,无须担心容量问题。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
在 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 个可选的缓存实现。
在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"/>
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
使用 Redis 前,必须有一个 Redis 服务,有关Redis安装启动的相关内容,可参考如下地址中的官方文档:https://redis.io/topics/quickstart。Redis服务启动后,在src/main/resources
目录下新增 redis.properties
文件 。
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=123456
database=0
clientName=
在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 的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享
。
储存位置如下:
github工程地址:https://github.com/nestorbian/spring-boot-examples/tree/master/mybatis-demo
Mybatis系列文章
Mybatis之配置详解
Mybatis之XML 映射器详解
Mybatis之动态 SQL详解
Mybatis之缓存详解
Mybatis之映射器注解详解
Mybatis之Mybatis Generator vs IDEA插件 自动生成代码
参考:
mybatis缓存
MyBatis缓存介绍
mybatis 缓存的使用, 看这篇就够了
mybatis缓存机制