该文章是综合在网上一些对mybatis二级缓存的原理解析。
像大多数的持久化框架一样,Mybatis也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。Mybatis 中缓存分为一级缓存,二级缓存。
一级缓存
一级缓存只是相对于同一个SqlSession而言。如果在参数和SQL完全一样的情况下,使用同一个SqlSession对象调用一个Mapper方法,只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
一级缓存的生命周期有多长?
- MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
- SqlSession中执行了commit操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
源码解析
由前面Mybatis执行过程的分析可以知道,Mybatis是将任务交给Executor,在由Executor交给Handler实现增删改查。
在BaseExecutor中,发现Executor执行查询前,会先去查询缓存。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//设置一级缓存的key
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++this.queryStack;
//判断resultHandler是否为空,为空就从缓存中根据一级缓存的KEY获取值
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
//判断缓存是否为空
if (list != null) {
//操作本地缓存输出
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果为空,就执行去数据库查询
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
且BaseExecutor中由两个属性
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
而PerpetualCache是接口Cache的实现类
它用一个Map来维护缓存
private Map<Object, Object> cache = new HashMap();
再来看一下key的产生方法
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
Iterator var8 = parameterMappings.iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
所以statementId 、 rowBounds 、传递给JDBC的SQL 和 rowBounds.limit决定key中的hashcode。
二级缓存
二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession 的。
从上面三张图中我们得出结论,一级缓存是sqlsession级别、二级缓存是Mapper级别。mybatis定义了【Cache】接口,支持自定义缓存,同时还支持集成第三方缓存库,现在为了更细粒度的控制缓存,更多的集成【ehcache】、【redis】。
那么mybatis的二级缓存主要是在Executor对象上来做文章,当mybatis发现你在mybatis.xml配置文件中设置cacheEnabled=true时,mybatis在创建sqlsession时创建Executor对象,同时会对Executor加上装饰者【CacheExecutor】。CacheExecutor对于查询请求,会判断application级别的二级缓存是否有缓存结果,如果有查询结果则直接返回,如果没有再交给查询器Executor实现类,也就是【SimpleExecutor】来执行查询。再就是缓存结果,返回给用户。
配置方式
1.在主配置文件中加入cacheEnabled=true配置,其实这里不用配置,默认情况下打开的。
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
2.再在映射配置文件中加入<cache>配置
<!--
eviction LRU
flushInterval缓存时间,以毫秒为单位
size缓存大小
readOnly如果为false的话,缓存对象必须是可序列化的-->
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="120000"
size="1024"
readOnly="true"/>
这里eviction属性有三个值可选:
LRU:(Least Recently Used),最近最少使用算法,即如果缓存中容量已经满了,会将缓存中最近做少被使用的缓存记录清除掉,然后添加新的记录;
FIFO:(First in first out),先进先出算法,如果缓存中的容量已经满了,那么会将最先进入缓存中的数据清除掉;
Scheduled:指定时间间隔清空算法,该算法会以指定的某一个时间间隔将Cache缓存中的数据清空;
3.statement语句的配置
flushCache
在映射文件的statement标签,可以设置是否刷新缓存。
flushCache = "true" 时可以刷新当前的二级缓存。
在默认情况下:
-
select语句:flushCache是false,也就是默认情况下,select语句是不会刷新缓存的。如果设置成true,那么每次查询都会去数据库查询,意味着查询的二级缓存失效;
-
insert、update、delete语句:flushCache是true,也就是默认情况下,增删改是会刷新缓存的。
useCache
默认情况下是true,即该statement使用二级缓存;当statement中设置userCache=false,可以禁用当前select语句的二级缓存,即每次查询都是去数据库中查询。
Mybatis整合使用ehcache
为什么使用第三方缓存框架?
Mybatis它是一个持久层的框架,不是一个缓存框架。所以说它本身的缓存机制不是很好,不能支持分布式,所以需要对它进行整合,整合其他的分布式缓存框架,ehcache和redis都可以。
整合缓存框架
使用mybatis默认的二级缓存时,在映射文件中只写了一个cache,其中就是默认使用的mybatis默认的二级缓存,也就是这个实现类,如果整合其他缓存框架的话,那么就需要改变了。
1.添加ehcache的jar包
2.添加ehcache的配置文件,配置文件如下
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2 xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
3 <!-- 缓存数据要存放的磁盘地址 -->
4 <diskStore path="F:\develop\ehcache" />
5 <!-- diskStore:指定数据在磁盘中的存储位置。 defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
6 以下属性是必须的: maxElementsInMemory - 在内存中缓存的element的最大数目 maxElementsOnDisk
7 - 在磁盘上缓存的element的最大数目,若是0表示无穷大 eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
8 overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 以下属性是可选的: timeToIdleSeconds
9 - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
10 timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大 diskSpoolBufferSizeMB
11 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区. diskPersistent
12 - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。 diskExpiryThreadIntervalSeconds
13 - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作 memoryStoreEvictionPolicy
14 - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出) -->
15
16 <defaultCache maxElementsInMemory="1000"
17 maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false"
18 timeToIdleSeconds="120" timeToLiveSeconds="120"
19 diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU">
20 </defaultCache>
21 </ehcache>
3.在映射文件中设置cache标签的type
这样就整合完成了。
二级缓存的优缺点
优点:对于查询多,commit少且用户对查询结果实时性要求不高的业务,此时采用mybatis二级缓存技术降低数据库访问量,提高访问速度。
缺点:二级缓存也有很多弊端,从MyBatis默认二级缓存是关闭的就可以看出来。
二级缓存是建立在同一个namespace下的,如果对表的操作查询可能有多个namespace,那么得到的数据就是错误的。
举个简单的例子:
订单和订单详情,orderMapper、orderDetailMapper。在查询订单详情时我们需要把订单信息也查询出来,那么这个订单详情的信息被二级缓存在orderDetailMapper的namespace中,这个时候有人要修改订单的基本信息,那就是在orderMapper的namespace下修改,他是不会影响到orderDetailMapper的缓存的,那么你再次查找订单详情时,拿到的是缓存的数据,这个数据其实已经是过时的。
根据以上,想要使用二级缓存时需要想好两个问题:
1)对该表的操作与查询都在同一个namespace下,其他的namespace如果有操作,就会发生数据的脏读。
2)对关联表的查询,关联的所有表的操作都必须在同一个namespace。