您的位置:首页 > 其它

学习Mybatis(8):Mybatis缓存机制的内部实现

2018-12-31 21:34 211 查看

在 学习Mybatis(7):Mybatis运行原理源码分析 中,根据源码可以看到,每个SqlSession都包含一个Executor对象

Mybatis的一切CRUD操作实际上就是通过Executor完成的

Executor默认是在DefaultSqlSessionFactory的openSessionFromDataSource方法中,调用Configuration类的newExecutor方法创建的,源码如下:

[code]    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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 (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}

Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}

最核心的就是中间的两段分支语句,第一段分支通过命令模式选择子类,第二段分支根据cacheEnable使用装饰器模式

cacheEnable属性的设置是在SqlSessionFactoryBuilder解析配置文件时进行的:

[code]    private void settingsElement(XNode context) throws Exception {
……
this.configuration.setCacheEnabled(this.booleanValueOf(props.getProperty("cacheEnabled"), true));
……
}

并且默认为true,即默认开启缓存机制(二级缓存,很多文章都说要显式配置cacheEnabled属性才会开启,实际上不是这样的!

1.一级缓存

一级缓存实际设置在BatchExecutor、ReuseExecutor和SimpleExecutor的基类BaseExecutor中:

[code]public abstract class BaseExecutor implements Executor {
……
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
……
}

当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在localCache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入localCache,最后返回结果给用户:

在CRUD时,会根据Sql语句生成CacheKey,看名字知作用,cachekey就是作为缓存键的:

[code]    //BaseExecutor
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());
……    //此处不断取出ParameterMapping,解析Sql参数,用来更新cahceKey
return cacheKey;
}
}

//CacheKey
//update方法实际调用doUpdate,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
++this.count;
this.checksum += (long)baseHashCode;
baseHashCode *= this.count;
this.hashcode = this.multiplier * this.hashcode + baseHashCode;
this.updateList.add(object);
}

这里如果cachekey发生了update,那么hashcode就会改变,key值就发生了变化,就是不同的缓存项了

然后在BaseExecutor的方法(如query)中查询缓存:

[code]    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
……

List list;
try {
++this.queryStack;
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);
}
} finally {
--this.queryStack;
}
……
return list;
}
}

可以看到,如果缓存命中,就进一步查询本地输出参数缓存,否则从数据库执行查询

PerpetualCache实现非常简单,就是一个HashMap存放数据:

[code]public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap();
……
}

一级缓存默认是SESSION级,即每个SqlSession共享一个缓存,可以通过setting标签的localCacheScope属性配置为STATEMENT,即每个语句拥有自己独立的缓存

时序图如下:(搬运自网络,侵删)

2.二级缓存

使用如 学习Mybatis(3):缓存 所述,在mapper xml中加一行 <cache/> 就行(当然实际可配置属性很多,详见官方文档)

上面提到,二级缓存其实也是默认开启的,只是没有配置的话,mybatis不会默认读写二级缓存而已

二级缓存的操作在CachingExecutor完成,以query方法为例:

[code]    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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, parameterObject, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}

return list;
}
}

return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

意思很简单,首先看程序员有没有在mapper中写<cache/>,写了的话就可以获取到二级缓存,然后按照 二级缓存-一级缓存 的顺序进行查询,没写(两种情况,一种是配置不对导致getCache结果为空,一种是根本就没写)的话就直接按照一级缓存的方式处理

Cache的实现类:

看名字知实现,使用了装饰器模式,分析代码可知,具体的装饰链是

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

  • SynchronizedCache: 同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache: 日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

flushCacheIfRequired也是看名字知作用系列,明显是根据需要刷新缓存的(不需要的情况:select语句,或者增删改语句flushCache属性未设置或者为false)

CachingExecutor的TransactionalCacheManager(即上面代码中的tcm)包含了一个HashMap:

[code]private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

这个Map保存了Cache和TransactionalCache的映射关系。
TransactionalCache实现了Cache接口,CachingExecutor会默认使用它包装Cache为TransactionalCache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。

往下,isUseCache方法就是检查useCache属性是否为true

再往下就是查询缓存已经未命中时向一级缓存、数据库查询并存入二级缓存的代码,最后返回查询的结果列表

注意putObject,实际上没有立刻存入缓存或者从缓存清除,而是先放进Map,等待事务提交时再处理:

[code]    public void putObject(Object key, Object object) {
this.entriesToRemoveOnCommit.remove(key);
this.entriesToAddOnCommit.put(key, new TransactionalCache.AddEntry(this.delegate, key, object));
}

public void commit() {
Iterator i$;
if (this.clearOnCommit) {
this.delegate.clear();
} else {
i$ = this.entriesToRemoveOnCommit.values().iterator();

while(i$.hasNext()) {
TransactionalCache.RemoveEntry entry = (TransactionalCache.RemoveEntry)i$.next();
entry.commit();
}
}

i$ = this.entriesToAddOnCommit.values().iterator();

while(i$.hasNext()) {
TransactionalCache.AddEntry entry = (TransactionalCache.AddEntry)i$.next();
entry.commit();
}

this.reset();
}

这里的delegate不是Executor了,而是Cache,也就是具体配置的二级缓存实现类负责缓存清理

至此二级缓存源代码分析完毕

时序图如下(同样侵删):

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: