你真的了解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 设置缓存级别

一级缓存的范围有SESSIONSTATEMENT两种,默认是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。我们不妨先来看下CachingExecutorquery方法:

@Override

public <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。我们来看下CachingExecutorcreateCacheKey的方法:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql){

return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);

}

delegate是委托对象。所以我们重点来看下BaseExecutorcreateCacheKey的方法:

 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。在CacheKeyupdate方法中,会进行一个hashcodechecksum的计算,同时把传入的参数添加进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的流程,代码如下所示:

@Override

publicintinsert(String statement, Object parameter){

return update(statement, parameter);

}

@Override

publicintdelete(String statement){

return update(statement, null);

}

update方法也是委托给了Executor执行,每次执行update方法都会清空缓存如下所示:

@Override

publicintupdate(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是不是很熟悉,好像和cachecache-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,当同时使用了cachecache-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 的查询方法:

@Override

public <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在清除缓存时,调用了TransactionalCacheclear方法,清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:

@Override

public 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就想起刚才TrancationalCacheclear方法设置的标志位,真正的清理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 清除缓存

CachingExecutorupdate里有刷新缓存的操作。而在DefaultSqlSession执行insert|update|delete的话,会统一进入CachingExecutorupdate方法,清空缓存。

@Override

public int update(MappedStatement ms, Object parameterObject) throws SQLException {

flushCacheIfRequired(ms);

return delegate.update(ms, parameterObject);

}

4.总结

  • 二级缓存在一级缓存执行之前执行
  • 使用二级缓存可以在mapper.xml中定义<cache><cacheRef>标签,两个同时设置,会以设置为准。
  • 二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  • 二级缓存在安全使用上较为苛刻。如分布式环境,多变查询等条件下,极大可能出现脏数据。建议直接使用RedisMemcached等分布式缓存可能成本更低,安全性也更高

以上是 你真的了解Mybtatis的缓存机制吗? 的全部内容, 来源链接: utcz.com/a/28279.html

回到顶部