一.缓存机制介绍
在默认情况下,Mybatis会启用缓存,俗称一级缓存,没办法关闭的。。 当然可以配置缓存,俗称二级缓存。当初学Mybatis的时候,视频里的老师说一级缓存的生命周期是整个SqlSession,二级缓存的生命周期是整个Mapper.xml ,云里雾里的。接下来以源码的角度看看缓存是如何运作的吧。
Mybatis在查询的时候,会先查二级缓存,如果没有,再查一级缓存。就以sql执行流程开始分析。
二.缓存分析
SqlSession是sql执行的关键,SqlSession里面的 Executor 是缓存的关键。先来回顾一下SqlSession的创建过程:
DefaultSqlSessionFactory:
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:
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; }
默认情况下,都会用 CachingExecutor包装一下,接下来关注CachingExecutor的query方法
CachingExecutor:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); //创建cacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
Mybatis是如何判断多次请求该用相同的缓存呢?答案是 CacheKey,看看创建过程:
BaseExecutor
/** * 1. 传入的 statementId * * 2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示); * * 3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() ) * * 4. 传递给java.sql.Statement要设置的参数值 * 4者相同,表明可以使用相同的一级缓存 * @param ms * @param parameterObject * @param rowBounds * @param boundSql * @return */ @Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); //statementId cacheKey.update(ms.getId()); //rowBound.offset cacheKey.update(rowBounds.getOffset()); //limit cacheKey.update(rowBounds.getLimit()); //sql语句 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); } //将每一个要传递给JDBC的参数值也更新到CacheKey中 cacheKey.update(value); } } if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
CacheKey:
public void update(Object object) { //得到对象的hashCode int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); //对象计数器 count++; checksum += baseHashCode; baseHashCode *= count; //计算hashCode,后续 equals()方法根据hashCode判断是否相等 hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); }
public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; }
关键问题解决了,接下来就要用到二级缓存了:
CachingExecutor:
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //二级缓存,保存缓存数据,属于MappedStatement级别,<cache/>标签解析的结果 Cache cache = ms.getCache(); //从 MappedStatement 中获取 Cache,注意这里的 Cache不是CacheExecutor创建的 //<cache/> 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) { //在这里面会调用 BaseExecutor的query查询---》一级缓存 --》数据库 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //缓存结果,保存到cache当中,而不是 TransactionalCacheManager(tcm), tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
二级缓存关键代码: List<E> list = (List<E>) tcm.getObject(cache, key);tcm是TransactionalCacheManager,可以理解为事务缓存管理器,为啥要戴上事务呢? 一起看看
TransactionalCacheManager:
public Object getObject(Cache cache, CacheKey key) { //cache用TransactionalCache包装过了,取出缓存 return getTransactionalCache(cache).getObject(key); }
private TransactionalCache getTransactionalCache(Cache cache) { //将cache传入TransactionalCache进行包装,但是cache可能已经有数据了,cache是二级缓存 return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); }
public void putObject(Cache cache, CacheKey key, Object value) { //设置值到 缓存中 getTransactionalCache(cache).putObject(key, value); }
TransactionalCache:
@Override public Object getObject(Object key) { // issue #116,从共享缓存中取数据 Object object = delegate.getObject(key); if (object == null) { //未命中缓存,将key存入entriesMissedInCache entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } }
@Override public void putObject(Object key, Object object) { // 将键值对存入到 entriesToAddOnCommit 中,而非 delegate 缓存中 entriesToAddOnCommit.put(key, object); }
可以看到,在TransactionalCache中,getObject 和 putObject 操作的变量不是同一个变量,这就是事务缓存的关键。
看看TransactionalCache中的字段
/**
* 共享缓存,保证多个事务不会读到脏数据,但无法解决不可重复读
*/
private final Cache delegate;
private boolean clearOnCommit;
// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中(事务缓存)
private final Map<Object, Object> entriesToAddOnCommit;
// 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
private final Set<Object> entriesMissedInCache;
对于未提交的事务,所有结果都会被暂时保存到entriesToAddOnCommit变量中,而delegate中才是真正保存事务提交后的数据。那么,数据是如何从 entriesToAddOnCommit ----> delegate 中的呢?触发条件是 SqlSession.commit() 方法
DefaultSqlSession:
public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
CachingExecutor:
public void commit(boolean required) throws SQLException { //删除一级缓存中的数据 delegate.commit(required); //刷新事务数据到二级缓存 tcm.commit(); }
TransactionalCacheManager:
public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { //刷数据到 真正的二级缓存 txCache.commit(); } }
TransactionalCache:
public void commit() { if (clearOnCommit) { delegate.clear(); } //数据迁移 flushPendingEntries(); reset(); }
/** * 事务提交时触发,避免读到脏数据 */ private void flushPendingEntries() { //将entriesToAddOnCommit中的数据 放到 delegate中 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { //存 null delegate.putObject(entry, null); } } }
再来一张事务缓存示例图:
二级缓存分析完,接下来分析一级缓存。当二级缓存中获取不到数据,这时候就来到了一级缓存
BaseExecutor:
@Override 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++; //从一级缓存中获取缓存项,从这里可以看出 mybatis无法关闭一级缓存。。 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(); /** * 一级缓存存在的弊端,在集群节点中,如果两台服务器上查询同一语句,此时缓存内容相同 * 如果一台执行更新,重新查询后,会将更新后的数据存入缓存,此时两条服务器中的一级缓存 * 内容不一致 * * 通过设置 <settings> * <setting name="localCacheScope",value="STATEMENT"/>,默认session * </settings> * 本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 * */ if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482,清空缓存,此时一级缓存失效。。 clearLocalCache(); } } return list; }
PerpetualCache:
private Map<Object, Object> cache = new HashMap<>();
public Object getObject(Object key) { //一级缓存,简单的 HashMap return cache.get(key); }
public void putObject(Object key, Object value) { cache.put(key, value); }
一级缓存相对来说比较简单,就是一个HashMap,接下来研究一下一级缓存的生命周期,为什么说是基于SqlSession的的生命周期。一级缓存在BaseExecutor中,SqlSession没了,localCache不就没了嘛。。
protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); //一级缓存 this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
DefaultSqlSession:
public void commit() { commit(false); } @Override public void commit(boolean force) { try { executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
BaseExecutor:
@Override public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } //清除一级缓存,因为 commit意味者sqlSession生命周期结束(但是可以主动调用commit) clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } }
@Override public void clearLocalCache() { if (!closed) { //清除一级缓存 localCache.clear(); localOutputParameterCache.clear(); } }
二级缓存,是从 MappedStatement对象中拿出 Cache,在事务提交后数据还是会保存到Cache中。即使 SqlSession被销毁了,还是可以从MappedStatement中拿出来, Cache中的数据依旧保留。
CachingExecutor:
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //二级缓存,保存缓存数据,属于MappedStatement级别,<cache/>标签解析的结果 Cache cache = ms.getCache(); //从 MappedStatement 中获取 Cache,注意这里的 Cache不是CacheExecutor创建的 //<cache/> 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) { //在这里面会调用 BaseExecutor的query查询---》一级缓存 --》数据库 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //缓存结果,保存到cache当中,而不是 TransactionalCacheManager(tcm), tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
三.总结
缓存机制总体来说还是比较简单的,在实际开发中,一般是不会使用Mybatis的二级缓存的。更多的是在业务层使用其他Nosql。
二级缓存是基于一个Mapper.xml文件的,当一个Mapper中的sql有涉及到连表查询,而另一个Mapper中涉及到相关表的更新,插入操作,这样缓存中的数据会不同步。当然,这种情况可以用<cache-ref>解决,多个Mapper指向同一个缓存,这样无疑加粗了缓存的粒度,任何更新/插入/删除 操作都会清空缓存,导致命中率下降。
上一张流程图:
结语:寻寻觅觅,冷冷清清,凄凄惨惨戚戚 !(无病呻吟!嘿嘿)