Mybatis源码阅读之--本地(一级)缓存实现原理分析
前言:
Mybatis为了提升性能,内置了本地缓存(也可以称之为一级缓存),在mybatis-config.xml中可以设置localCacheScope中可以配置本地缓存的作用域,包含两个值session和statement,其中session选项表示本地缓存在整个session都有效,而statement只能在一条语句中有效(这条语句有嵌套查询--nested query/select)。
下面分析一下mybatis本地缓存的实现原理。
本地缓存是在Executor内部构建,Executor包含了四个实现类,SimpleExecutor,BatchExecutor以及CachingExecutor和RoutingExecutor,其中CachingExecutor是开启了二级缓存才会用到的,这里先不说,而RoutingExecutor是负责路由的,它本身包含了Executor,也先不管,这里主要是SimpleExecutor和BatchExecutor,他们都实现了BaseExecutor,而BaseExecutor中正是进行了一级缓存的处理。
public abstract class BaseExecutor implements Executor { protected PerpetualCache localCache; // 一级缓存,实质就是一个HashMap<Object, Object> protected PerpetualCache localOutputParameterCache; // 出参一级缓存,当statment为callable的时候使用 }
在BaseExecutor中定义了一个PerpetualCache类型的localCache属性,用来保存一级缓存
而PerpetualCache类的主要功能如下:
public class PerpetualCache implements Cache { private final String id; // 该缓存的id private final Map<Object, Object> cache = new HashMap<>(); // ...其他一些获取缓存数据、移除缓存数据的方法 }
其中包含了两个属性,id表示缓存的唯一标识,cache是一个HashMap类型的对象,里面存放所有已经缓存的数据
也就是是说Mybatis的一级缓存实质就是一个HashMap。
再回过头看一看BaseExecutor中的一级缓存处理过程(下述中的代码片段都是BaseExecutor类中的,不会再把类加上了):
- select添加缓存
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 获得缓存键 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 根据cachekey执行查询 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
首先创建缓存键key,然后根据key再查询。
下面代码展示了根据key进行查询的逻辑
@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++; 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(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
先看第二个if--如果当前的查询语句设置了清除缓存的属性为true,那么就要把一级缓存清除
当然里面还需要满足queryStack==0的条件,这个条件涉及到了嵌套查询(nested select/query),如果是嵌套查询的最外层查询(第一个查询),才进行缓存的清理动作,否则不进行。这里的queryStack是查询的层级,取决于nested select的层数,例如一个Blog有一个Author,一个Author有一个Account,其中Author和Account都使用了嵌套查询,并且不是延迟加载(fetchType设置),那么Author查询的时候queryStack就会是1,Account查询的时候queryStack为2。针对嵌套查询这里就说这么多,后续会专门写一篇嵌套查询原理的文章,包括非延迟加载以及延迟加载的不同情况的处理方式。
if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }
下面代码片段展示了从缓存中取数据的逻辑
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; `` 直接调用localCache的getObject方法,但是需要在resultHandler不为null的情况,因为如果查询数据是传入了ResultHandler,那么会返回null,数据由ResultHandler进行处理。 如果缓存中查到了数据,那么会处理缓存的出参(出参只有在MappedStatement类型为Callable时才会有,其他的STATEMENT/PREPAREDSTATMENT都没有) 如果没有查到数据,那么从数据库中查询 ```java list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
数据查询完了之后执行queryStack--操作。
进入queryFromDatabase方法进行分析:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 这里为什么要先占位呢? // 回答:嵌套的延迟加载有可能用的是同一个对象,这里说明已经开始查了, // 但是由于处理嵌套的查询,此查询还没有查完,再次执行嵌套查询,且查询的是相同的东西,那么就不用再查了 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 删除占位 localCache.removeObject(key); } // 将数据放入到缓存中 localCache.putObject(key, list); // 对于callable的statement来说,出参也需要缓存,而出参也是放在了入参中 // 因此这里缓存了入参 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
主要步骤:
- 先在一级缓存中设置一个占位符,EXECUTION_PLACEHOLDER,
此处代码的作用就是为了防止嵌套查询是查询了相同的数据
举个例子,一个Blog有一个Author,而Author中又嵌套了一个Blog,那么Blog还没有放到缓存中,但是嵌套查询现在查Author,Author中的Blog又是第一个Blog查询的数据,这里放置一个占位符就是为了说明,这个Blog已经在查询了,结果还没出来而已,不要急,等结果出来了再进行配对。 - 执行子类的doQuery方法,查询数据
- 删除缓存占位、将查询出的数据放入到缓存中。
- 如果此查询语句是CALLABLE类型的,那么要把出参也缓存
以上四部做完之后从数据库中查询数据就结束了,其中第一步可能有些人还是很困惑,大家可以执行一些测试看一看。
再次将思路返回到query方法中,
if (queryStack == 0) { // 最外层的查询已经结束 // 所有非延迟的嵌套查询也已经查完了,那么就可以把嵌套查询的结果放入到需要的对象中 for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } }
当最外层查询结束时,需要执行一些清理动作:
- 执行所有嵌套查询的连接操作,上面例子中的Blog->Author->Blog,会把author中的Blog设置正确
- 清除嵌套查询
- 如果当前语句的一级缓存作用域是statement的话,要把一级缓存清空
上面的第一步和第二部需要结合ResultSetHandler共同分析,后面分析嵌套查询的时候再做详细的介绍,这里大家心中有个了解即可。
至此,查询过程的缓存处理就已经结束了
下面简单看一下cleanLocalCache方法
public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } }
也很简单,就是把localCache和localOutputParameterCache置空。
接下来就分析update(其中insert/update/delete都统称为update)时,一级缓存如何处理:
public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } // 先清除缓存 clearLocalCache(); // 使用子类的doUpdate方法 return doUpdate(ms, parameter); } `` 先把缓存清空,然后调用子类的doUpdate执行具体的更新操作 另外事务的提交以及回滚都会清空以及缓存,代码如下: ```java public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); // 清除缓存 flushStatements(); if (required) { transaction.commit(); } }
public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); // 清理缓存 flushStatements(); if (required) { transaction.commit(); } }
public void rollback(boolean required) throws SQLException { if (!closed) { try { clearLocalCache(); // 清理缓存 flushStatements(true); } finally { if (required) { transaction.rollback(); } } } }
因此,在一个sqlSession执行了commit或者rollback方法后,一级缓存已经没有了数据,如果再次执行相同的查询操作,那么会重新从数据库中查询。
一级缓存需要注意的事项:
在实际开发中,有可能对查询数据进行一些操作,比如修改一些字段,或者一个列表中删除/添加一些数据,再次执行相同的查询,返回的不会是数据库中的数据,而是经过修改的数据,因此最好不要对Mybatis返回的数据进行修改操作。