Mybatis提供对缓存对支持
- 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就 将清空,默认打开一级缓存。
- 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源, 如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要 实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置
- 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将 被 clear。
一级缓存
我们已经知道一级缓存是基于PerpetualCache的,现在来根据源码看一下PerpetualCache创建过程把
localCache创建
PerpetualCache创建时,是和Executor一起创建的,查看源码,创建sqlSession的代码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
进入configuration.newExecutor()
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
默认创建的是SimpleExecutor,进入
public SimpleExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
代码是直接使用父类的构造方法
```
第一个localCache就是我们的一级缓存咯
localCache的使用
关于缓存的使用,肯定是在Executor方法使用时使用
进入该方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
根据之前的知识,BoundSql是我们将要执行的sql语句,关于CacheKey,就是我们的缓存的key,无论一级缓存还是二级缓存,都是基于一个HashMap存储的
缓存Key值创建
下面看看key是怎么创建的
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
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();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
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 = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
根据一系列的cacheKey.update方法的传参,我们不难判断,在缓存的key创建,
- 根据MappedStatement的id(ms.getID)
- 查询时要求的结果集中的结果范围(rowBounds.getOffset,rowBounds.getLimit())
- 查询所产生的最终要传递给JDBC的Sql语句字符串(boundSql.getSql())
- 传递给Statement要设置的参数值(cacheKey.update(value))
经过上面的一系列update方法,所创建的缓存key如图所示
有兴趣的朋友可以看下update的具体方法,其实现主要就是维持一个名为updateList的ArrayList对象add一个object
回到query方法,缓存的key获得到了,接下来就是根据这个key去寻找值对吧
缓存key的get
进入下一个query方法
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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
关于第一个Cache对象,它是属于二级缓存的东西,我们暂时先不讲,接着进入下一个query方法
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 (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
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);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
来了list = resultHandler == null ? (List
这串代码是先判断resultHandler是不是为空,毋庸置疑,他就是为空的,判断为ture,调用一个方法localCache.getObject()
我们知道PerpetualCache里面有个cache对象,它是一个HashMap对象
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
}
直接通过gatKey的方法获得了HashMap中的value,就这样,获得了缓存value,是不是很简单
接着看看这个Map是在哪里put值的
缓存的value值
继续进入queryFromDatabase方法
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 {
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;
}
大家都知道hashMap的key是不能重复的,就是现在,查找到了list后,调用了一个localCache.putObject(key, list),就是这样一个简单的过程,put了一个以key为键的list为值的value
一级缓存清空
当Session flush或close之后,一级缓存将清空
close我们都知道,就是手动关闭,那必然会清空
其实对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉
来看下源码
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);
}
可以看见,在调用下一层的doUpdate方法前,进行了一次clearLocalCache(),点进去查看下源码
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
就这样,清空了一级缓存
一级缓存总结
- MyBatis对会话(Session)级别的一级缓存设计的比较简单,就简单地使用了HashMap来维护
- 只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉
- MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过期,因此也就没有对缓存的结果进行更新这一说了
二级缓存
默认情况下是只开启了局部的sqlSession缓存(一级缓存),打开二级缓存需要配置
- 在xml文件中
<settings>
<!--默认为true-->
<setting name="cacheEnabled" value="true"/>
</settings>
- 在sql映射文件中
<cache/>
- 实体类实现序列化接口
就这样,二级缓存就能使用了
关于cache
cache还有很多东西可以设置
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突
eviction即为回收策略
还有其他回收策略,分别为:
- LRU – 最近最少使用的:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认为LRU
flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
二级缓存的实现
二级缓存和一级缓存一样,都是在调用Executor时开始使用,接下来看看二级缓存是怎么实现的
回到之前configuration.newExecutor方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
关键词出现,也就是之前需要配置的cacheEnabled,它的值默认是true的
executor又被重新new成了一个CachingExecutor类型,我们点进去
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
噢~根据装饰模式看,原来它是Executor的装饰类,所以,实际上在设置cacheEnabled为false前,我们一直使用的Executor的装饰类,现在都清楚了
那我们就进入CachingExecutor的query方法把
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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
第二个关键字来了—Cache,他就是我们在sql映射文件配置的cache,它是一个接口,那不多说,查看它的实现类把
果然和我们之前配置的一样
所以,关于二级缓存的使用,一直是我们的装饰类CachingExecutor一直在暗箱操作
关闭二级缓存
关闭二级缓存有四种方式
- 主配置文件中设置cacheEnabled为false
< setting name=“cacheEnabled” value=“false”/ > - mapper配置文件中去掉< cache >
< cache eviction=“FIFO” flushInterval=“60000” size=“512” readOnly=“true”/ > - 将查询< select > 标签useCache属性设置为false
< select useCache=“false” > - 将查询< select > 标签flushCache属性设置为true
< select flushCache=“true” >
二级缓存原理
二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是一级缓存区域。
一级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。一级缓存中的value,就是查询出的结果对象。
一级缓存是默认使用的。
二级缓存需要手动开启。
在这里,顺便提一下,mybatis的二级缓存是属于序列化,序列化的意思就是从内存中的数据传到硬盘中,这个过程就是序列化
反序列化意思就是相反而已
也就是说,mybatis的二级缓存,实际上就是将数据放进了硬盘文件中去了
总结
简单点说
- 映射语句文件中的所有select语句将会被缓存。
- 映射语句文件中的所有insert,update和delete语句会刷新缓存。
- 二级缓存是基于一级缓存的,一级缓存的数据是存储在一个HashMap中,二级缓存数据是通过序列化放进了硬盘文件
评论区