Mybatis缓存
缓存
缓存是一般ORM框架都有的功能,目的就是提高查询的效率和减少数据库的压力。
缓存结构
Mybatis源码中与缓存相关的类都在cache包中,其中有一个Cache接口,默认实现类PerpetualCache,他是由HashMap实现的,是基础缓存。
Mybatis的缓存功能是采用装饰器模式实现的。
装饰器模式:在不改变原对象的基础上,将功能附加到对象上,提供了比继承更有弹性的代替方案。
缓存继承关系:
mybatis缓存总体分为三大类:基本缓存、淘汰算法缓存、装饰器缓存
一级缓存
一级缓存也叫本地缓存,Mybatis的一级缓存实在会话层进行缓存的。Mybatis的一级缓存默认是开启的,不需要任何的配置。伪关闭方法提高缓存级别(localCacheScope设置为STATEMENT,只针对statement有效)
BaseExecutor的query()
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
mybatis执行的流程里面,缓存对象PerpetualCache是哪个对象维护的呢?
Mybatis一级缓存是与SqlSession共存亡的,所以就不需要为SqlSession编号、再根据SqlSession的编号去查询对应的缓存了。
DefaultSqlSession里面有两个对象属性: Configuration和Executor
其中Configuration是全局的,不属于SqlSession,所以缓存维护在Executor里面--实际上他维护在基本执行器SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有PrepetualCache。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再去查询数据库。但不同的会话里面,执行相同的SQL,也会去查询数据库语句,不走一级缓存。
一级缓存验证
首先关闭二级缓存,localCacheScope设置为SESSION。
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
1.在同一个session中共享
UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));
System.out.println(mapper.selectOne(1));
2.不同session中不能共享
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));
一级缓存在BaseExecutor的query()--queryFromDatabase()中存入。在queryFromDatabase之前会get()。
//从缓存中获取数据(key是CacheKey)
//一级缓存和二级缓存的CacheKey是同一个
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 真正的查询流程
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 默认Simple
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 写入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
3.同一个会话中,update(包括delete)会导致一级缓存清空
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//先执行缓存清空操作
clearLocalCache();
return doUpdate(ms, parameter);
}
QA:只有更新才会清空缓存吗?查询会清空缓存吗?怎么清空?
一级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的,如果是query只有select标签的flushCache=true才清空。
一级缓存的工作范围是一个会话。如果跨回话,出现什么问题?
4.其他会话更新会导致当前会话读到的数据是过时的数据(不能跨会话共享)
//会话2更新数据
UserMapper mapper2 = session.getMapper(UserMapper.class);
mapper.updateById(user);
session.commit();
//会话1读取到过时的数据,一级缓存不能夸会话共享
System.out.println(mapper1.selectOne(1));
不足
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在查询到过时数据的问题。如果要解决这个问题,需要开启二级缓存。
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题。
QA:如果开启了二级缓存,是在一级缓存前面还是后面执行呢?怎么维护的?
作为一个作用范围更广的缓存,可定在SqlSession的外层,不然做不到SqlSession共享。
而一级缓存是在SqlSession内部的,所以是在一级缓存前面执行,只有二级缓存找不到才会去一级缓存找。
那么二级缓存在哪里维护的呢? 跨会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,所以应该在BaseExecutor之外创建。
但只有二级缓存开启后才能加载这个对象。
实际上Mybatis使用了一个装饰器类(CachingExecutor)来维护。
如果启用了二级缓存。Mybatis在创建Executor对象的时候会对Executor进行装饰。
CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,没有的话就交给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,
再走到一级缓存。最后把结果缓存起来,返回给用户。
二级缓存开启方式
1.在mybatis-config.xml中配置了(默认true)
<setting name="cacheEnable" value="true"/>
只要开启了二级缓存,都会使用CachingExecutor装饰基本的执行器(SIMPLE、REUSE、BATCH)
二级缓存默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要使用二级缓存,还要单独配置。
2.在Mapper.xml配置
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!--最大缓存个数,默认1024-->
eviction="LRU" <!--缓存策略-->
flushInterval="120000" <!-- 自动刷新时间,未配置时只有调用时刷新 -->
readOnly="false"/> <!-- 默认false,改为true可读可写,对象必须支持序列化 -->
cache属性详解:
Mapper.xml配置了
QA:如果cacheEnable=true,Mapper.xml没有配置
只要cacheEnable=true基本执行器就会被装饰。有没有配置<cache>,决定了在启用的时候能不能创建mapper这个Cache对象,最终会影响到CachingExecutor query方法里面的判断。
也就是说,此时会被装饰,但没有cache对象,依然不会走二级缓存。
QA:如果一个Mapper需要开启二级缓存,但是这里面的某些查询方法对数据实时性要求很高,不需要二级缓存,怎么办?
可以在单个Statement ID上显示关闭二级缓存(默认是true)
<select id="selectUser" resultMap="BaseResultMap" useCache="false">
CachingExecutor query方法的判断:
// cache 对象是在哪里创建的? XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
// flushCache="true" 清空一级二级缓存 >>
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 获取二级缓存
// 缓存通过 TransactionalCacheManager、TransactionalCache 管理
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 写入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
二级缓存验证
UserMapper mapper = session.getMapper(userMapper.class);
System.out.println(mapper.selectOne(1));
//事务不提交的情况下,二级缓存不会写入
session.commit();
UserMapper mapper2 = session2.getMapper(userMapper.class);
System.out.println(mapper2.selectOne(1));
QA:为什么事务不提交,二级缓存不生效?
因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache的getObject()、putObject和commit方法,TransactionCache
里面又持有真正的Cache对象,列入被层层装饰的PerpetualCache对象。
在putObject的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit()方法被调用的时候才会调用flushPendingEntries()真正写入缓存。
他就是在DefaultSqlSession调用commit()的时候被调用的。
~~~java
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 真正写入二级缓存
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
QA:为什么增删改会清空缓存?
在CachingExecutor的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面渠道的flushCache的值。而增删改操作的flush属性默认为true.
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 增删改查的标签上有属性:flushCache="true" (select语句默认是false)
// 一级二级缓存都会被清理
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
也就是说,如果不需要清空二级缓存,可以把flushCache属性修改成false(这样会造成过时数据的问题)。
二级缓存的使用场景
1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如交易历史、历史订单的查询。
2、如果多个namespace中有针对性同一个表的操作,比如user表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。
所以推荐在一个Mapper里面只操作单表的情况使用。
QA:怎么让多个namespace共享一个二级缓存?
跨namespace的缓存共享的问题,可以使用
<cache-ref namespace="com.xxx.xxx.dao.UserMapper"/>
cache-ref代表引用到别的命名空间的Cache配置,两个命名空间的操作使用是同一个Cache。在关联的表比较少,或者按照业务可以进行表进行分组的时候可以使用。
Ps:这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。
使用第三方作为二级缓存
除了Mybatis自带的二级缓存之外,我们也可以实现Cache接口自定义二级缓存。
例如集成redis做二级缓存:
https://github.com/mybatis/redis-cache
pom.xml文件依赖:
<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"/>
redis.properties配置:
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
Redis作为二级缓存的验证(需要安装Redis客户端):RedisManager
当然在分布式环境中也可以单独的使用缓存服务,不使用Mybatis自带的二级缓存。