学习Mybatis(8):Mybatis缓存机制的内部实现
在 学习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,也就是具体配置的二级缓存实现类负责缓存清理
至此二级缓存源代码分析完毕
时序图如下(同样侵删):
- NIO学习系列:缓冲区内部实现机制
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理(六)》 MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- mybatis 中应用二级缓存(使用框架本身实现的缓存机制)
- 【MyBatis学习13】一、二级缓存及分布式实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理(五)》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- MyBatis缓存机制的设计与实现
- MyBatis学习手记(二)MyBatis缓存机制
- MyBatis学习(五)-缓存机制
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- mybatis实现对象之间的关系(一对一、一对多、多对多)以及mybatis的缓存机制
- 深入理解mybatis原理(六) MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- 自己在项目中的学习总结:利用工厂模式+反射机制+缓存机制,实现动态创建不同的数据层对象接口
- MyBatis缓存机制学习笔记
- Mybatis学习(五)————— 延迟加载和缓存机制(一级二级缓存)
- NIO学习系列:缓冲区内部实现机制
- java教程、java学习:Java数据缓存实现的核心机制