你真的了解Mybtatis的缓存机制吗?
一级缓存
1.工作流程
在我们的应用与DB交互过程中,可能会出现在在一次的会话(SqlSession)中多次执行相同的SQl语句,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。执行流程如下图:
前面的文章介绍过,每一个SqlSession
中都持有一个Executor
,而每一个Executor
中都有一个LocalCache
。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement
,在Local Cache
进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache
,最后返回结果给用户。
2.生命周期
Mybatis的一级缓存是基于同一个SqlSession
的,他们的生命周期也是一致的。
- SqlSession在创建的时候会创建一个
Executor
对象,Executor
中有一个localCache
(PerpetualCache
对象)属性,PerpetualCache
就是缓存对象。 - 如果SqlSession调用了clearCache(),会清空
PerpetualCache
对象中的数据,但是该对象仍可使用。 - SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空
PerpetualCache
对象的数据,但是该对象可以继续使用。 - 如果SqlSession调用了close() 方法,会释放掉一级缓存
PerpetualCache
对象,一级缓存将不可用。
3.源码分析
3.1 设置缓存级别
一级缓存的范围有SESSION
和STATEMENT
两种,默认是SESSION,如果不想使用一级缓存,可以把一级缓存的范围指定为STATEMENT
,这样每次执行完一个Mapper中的语句后都会将一级缓存清除。
如果需要更改一级缓存的范围,可以在Mybatis的配置文件中,通过localCacheScope
指定。
<settingname="localCacheScope"value="STATEMENT"/>
如果不指定,默认是SESSION
级别,如下图解析xml代码:
privatevoidsettingsElement(Properties props){...
// 默认值使用SESSION
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
...
}
3.2 写缓存
3.2.1 获取缓存key
SqlSession在执行数据库操作时会委托给Executor执行。有一个和缓存有关Executor叫CachingExecutor
。我们不妨先来看下CachingExecutor
的query
方法:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException {
//获取sql语句
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//调用query方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里在查询前,先创建了一级缓存的key。我们来看下CachingExecutor
的createCacheKey
的方法:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql){return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
delegate是委托对象。所以我们重点来看下BaseExecutor
的createCacheKey
的方法:
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);
} elseif (parameterObject == null) {
value = null;
} elseif (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;
}
在上述的代码中,将MappedStatement
的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey
。在CacheKey
的update
方法中,会进行一个hashcode
和checksum
的计算,同时把传入的参数添加进updatelist
中。如下代码所示:
publicvoidupdate(Object object){int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
除去hashcode、checksum和count的比较外,只要updatelist
中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。
Statement Id + Offset + Limmit + Sql + Params
3.2.2 CachingExecutor是处理二级缓存的
我们继续回到CachingExecutor
的query方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException
{//获取 MappedStatement 中的 Cache cache 属性。
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
//是否
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//尝试使用key获取缓存
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;
}
}
// 交给委托类
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
代码很简单,无非就是一个判断缓存的过程。但是可能看到这里的同学有疑问了,我们刚才说一级缓存的对象不是localCache
吗,这里怎么是tcm
(TransactionalCacheManager
)? 你的怀疑没错,这里确实是缓存,传说中的二级缓存。也就是说,二级缓存的的判断在一级缓存之前。我们先记住这个结论,一会在探究二级缓存那些事。
那么问题来了,一级缓存在哪呢?
不妨往下看CachingExecutor
委托类BaseExecutor
的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) {
thrownew 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 {
//从db查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
//如果是STATEMENT级别,删除缓存。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
ok,就是在这里。如果查不到的话,就从数据库查,在queryFromDatabase中,会对 localCache
进行写入。在query方法执行的最后,会判断一级缓存级别是否是STATEMENT
级别,如果是的话,就清空缓存,这也就是STATEMENT
级别的一级缓存无法共享localCache的原因。
注意
:MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为STATEMENT
3.3 清除缓存
在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。
SqlSession的insert方法和delete方法,都会统一走update的流程,代码如下所示:
@Overridepublicintinsert(String statement, Object parameter){
return update(statement, parameter);
}
@Override
publicintdelete(String statement){
return update(statement, null);
}
update方法也是委托给了Executor执行,每次执行update方法都会清空缓存如下所示:
@Overridepublicintupdate(MappedStatement ms, Object parameter)throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
thrownew ExecutorException("Executor was closed.");
}
//清除缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
4.思考
- MyBatis的一级缓存设计的比较简单,就简单地使用了HashMap来维护,并没有对HashMap的容量和大小进行限制,有内存溢出的风险。
- 一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念。
- 多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为
Statement
。
二级缓存
1.工作流程
一级缓存最大的作用范围是同一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
二级缓存是基于namespace的,被多个SqlSession共享,是一个全局变量。
2.生命周期
mybatis的二级缓存是基于application为生命周期的。emmmm很持久。。
3.源码分析
3.1 开启缓存
3.1.1 cacheEnabled
还记得CachingExecutor
吗?他在初始化的时候又一个条件就是:
if (cacheEnabled) {executor = new CachingExecutor(executor);
}
如果 cacheEnabled
表示二级缓存机制标记,默认为true。缓存的实现类为 CachingExecutor
,这里使用了经典的装饰模式,处理了缓存的相关逻辑后,委托给的具体的 Executor 执行。
cacheEnabled
可以通过mybatis-config.xml 文件中指定。
<configuration><settings>
<settingname="cacheEnabled"value="true"/>
</settings>
</configuration>
3.1.2 cache和cache-ref标签
<cache>
标签用于声明当前namespace
使用二级缓存,并且可以自定义配置。
type
:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。eviction
: 定义回收的策略,常见的有FIFO,LRU。flushInterval
: 配置一定时间自动刷新缓存,单位是毫秒。size
: 最多缓存对象的个数。readOnly
: 是否只读,若配置可读写,则需要对应的实体类能够序列化。blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存
cache-ref
代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-refnamespace="mapper.XXX"/>
3.2 cache属性的创建
CachingExecutor
的query方法,一上来就有一步获取cache属性的操作:
Cache cache = ms.getCache();
ms就是MappedStatement对象,它是在MapperBuilderAssistant的addMappedStatement方法创建的。
继续来追踪这个currentCache
属性
useCacheRef和UseNewCache是不是很熟悉,好像和cache
和cache-ref
标签有点关系。
果不其然,一个是注解的解析,一个是xml解析。我们来看下xml解析。
private void cacheElement(XNode context) {if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
再来看下useCacheRef
:
public Cache useCacheRef(String namespace){if (namespace == null) {
thrownew BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//获取某命名空间的缓存
Cache cache = configuration.getCache(namespace);
if (cache == null) {
thrownew IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
//修改当前缓存
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
thrownew IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
对于同一个Mapper来讲,只能使用一个Cache,当同时使用了cache
和cache-ref
时,cache
定义的优先级更高,可以参考如下代码:
private void configurationElement(XNode context) {try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
...
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
3.3 创建缓存
我们再次回看 CacheingExecutor 的查询方法:
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException {
//获取sql语句
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//调用query方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
前面已经介绍过createCacheKey了,这里就不多提了,继续看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")
//取缓存
//如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率
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;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3.4 TransactionalCache 操作缓存
flushCacheIfRequired(ms)
如果不是查询语句的话,会清空缓存。
接下来我们说下tcm——TransactionalCacheManager
,实际上它是一个Map:
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
这个Map保存了Cache
和用TransactionalCache包装后的Cache
的映射关系。
TransactionalCache
实现了Cache
接口,CachingExecutor
会默认使用他包装初始生成的Cache
,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
publicclassTransactionalCacheimplementsCache{privatestaticfinal Log log = LogFactory.getLog(TransactionalCache.class);
//真实缓存对象
privatefinal Cache delegate;
//是否需要清空提交空间的标识
privateboolean clearOnCommit;
//所有待提交的缓存
privatefinal Map<Object, Object> entriesToAddOnCommit;
//未命中的缓存集合,防止击穿缓存,并且如果查询到的数据为null,说明要通过数据库查询,有可能存在数据不一致,都记录到这个地方
privatefinal Set<Object> entriesMissedInCache;
flushCacheIfRequired
在清除缓存时,调用了TransactionalCache
的clear
方法,清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:
@Overridepublic void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
写缓存调用tcm
的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。
publicvoidputObject(Object key, Object object){entriesToAddOnCommit.put(key, object);
}
这样看来,putObject
并未直接对二级缓存造成影响。一切还是要等到commit
方法执行。
public void commit() {if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
它的调用链路是:
TransactionalCacheManager-> CachingExecutor
-> DefaultSqlSession
-> SqlSessionTemplate
看到这里的clearOnCommit
就想起刚才TrancationalCache
的clear
方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries
方法,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。代码如下所示:
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);
}
}
}
3.6 清除缓存
CachingExecutor
的update
里有刷新缓存的操作。而在DefaultSqlSession执行insert|update|delete
的话,会统一进入CachingExecutor
的update
方法,清空缓存。
@Overridepublic int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
4.总结
- 二级缓存在一级缓存执行之前执行
- 使用二级缓存可以在
mapper.xml
中定义<cache>
和<cacheRef>
标签,两个同时设置,会以设置为准。 - 二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到
namespace
级别,通过Cache
接口实现类不同的组合,对Cache
的可控性也更强。 - 二级缓存在安全使用上较为苛刻。如分布式环境,多变查询等条件下,极大可能出现脏数据。建议直接使用
Redis
、Memcached
等分布式缓存可能成本更低,安全性也更高
以上是 你真的了解Mybtatis的缓存机制吗? 的全部内容, 来源链接: utcz.com/a/28279.html