首页 >Java >java教程 >【MyBatis源码解析】MyBatis一二级缓存

【MyBatis源码解析】MyBatis一二级缓存

巴扎黑
巴扎黑原创
2017-06-26 10:00:341418浏览

MyBatis缓存

我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。

MyBatis的缓存分为两种:

  1. 一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库

  2. 二级缓存,二级缓存是Mapper级别的缓存,定义在Mapper文件的标签中并需要开启此缓存,多个Mapper文件可以共用一个缓存,依赖标签配置

下面来详细看一下MyBatis的一二级缓存。

 

MyBatis一级缓存工作流程

接着看一下MyBatis一级缓存工作流程。前面说了,MyBatis的一级缓存是SqlSession级别的缓存,当openSession()的方法运行完毕或者主动调用了SqlSession的close方法,SqlSession就被回收了,一级缓存与此同时也一起被回收掉了。前面的文章有说过,在MyBatis中,无论selectOne还是selectList方法,最终都被转换为了selectList方法来执行,那么看一下SqlSession的selectList方法的实现:

 1 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 2     try { 3       MappedStatement ms = configuration.getMappedStatement(statement); 4       return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5     } catch (Exception e) { 6       throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e); 7     } finally { 8       ErrorContext.instance().reset(); 9     }10 }

继续跟踪第4行的代码,到BaseExeccutor的query方法:

1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2     BoundSql boundSql = ms.getBoundSql(parameter);3     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);4     return query(ms, parameter, rowBounds, resultHandler, key, boundSql);5 }

第3行构建缓存条件CacheKey,这里涉及到怎么样条件算是和上一次查询是同一个条件的一个问题,因为同一个条件就可以返回上一次的结果回去,这部分代码留在下一部分分析。

接着看第4行的query方法的实现,代码位于CachingExecutor中:

 1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2       throws SQLException { 3     Cache cache = ms.getCache(); 4     if (cache != null) { 5       flushCacheIfRequired(ms); 6       if (ms.isUseCache() && resultHandler == null) { 7         ensureNoOutParams(ms, parameterObject, boundSql); 8         @SuppressWarnings("unchecked") 9         List<E> list = (List<E>) tcm.getObject(cache, key);10         if (list == null) {11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12           tcm.putObject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }

第3行~第16行的代码先不管,继续跟第17行的query方法,代码位于BaseExecutor中:

 1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); 3     if (closed) { 4       throw new ExecutorException("Executor was closed."); 5     } 6     if (queryStack == 0 && ms.isFlushCacheRequired()) { 7       clearLocalCache(); 8     } 9     List<E> list;10     try {11       queryStack++;12       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;13       if (list != null) {14         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);15       } else {16         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);17       }18     } finally {19       queryStack--;20     }21     ...22 }

看12行,query的时候会尝试从localCache中去获取查询结果,如果获取到的查询结果为null,那么执行16行的代码从DB中捞数据,捞完之后会把CacheKey作为key,把查询结果作为value放到localCache中。

MyBatis一级缓存存储流程看完了,接着我们从这段代码中可以得到三个结论:

  1. MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor

  2. MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为"localCache"的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果

  3. MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将标签所在的Mapper的Namespace+标签中定义的sql语句

即只要两次查询满足以上三个条件且没有定义flushCache="true",那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。

 

MyBatis二级缓存

上面说完了MyBatis,接着看一下MyBatis二级缓存,还是从二级缓存工作流程开始。还是从DefaultSqlSession的selectList方法进去:

 1 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 2     try { 3       MappedStatement ms = configuration.getMappedStatement(statement); 4       return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5     } catch (Exception e) { 6       throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e); 7     } finally { 8       ErrorContext.instance().reset(); 9     }10 }

执行query方法,方法位于CachingExecutor中:

1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2     BoundSql boundSql = ms.getBoundSql(parameterObject);3     CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);4     return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);5 }

继续跟第4行的query方法,同样位于CachingExecutor中:

 1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2       throws SQLException { 3     Cache cache = ms.getCache(); 4     if (cache != null) { 5       flushCacheIfRequired(ms); 6       if (ms.isUseCache() && resultHandler == null) { 7         ensureNoOutParams(ms, parameterObject, boundSql); 8         @SuppressWarnings("unchecked") 9         List<E> list = (List<E>) tcm.getObject(cache, key);10         if (list == null) {11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12           tcm.putObject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }

从这里看到,执行第17行的BaseExecutor的query方法之前,会先拿Mybatis二级缓存,而BaseExecutor的query方法会优先读取MyBatis一级缓存,由此可以得出一个重要结论:假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存

而第3行~第16行的逻辑:

  • 第5行的方法很好理解,根据flushCache=true或者flushCache=false判断是否要清理二级缓存

  • 第7行的方法是保证MyBatis二级缓存不会存储存储过程的结果

  • 第9行的方法先尝试从tcm中获取查询结果,这个tcm解释一下,这又是一个装饰器模式(数数MyBatis用到了多少装饰器模式了),创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的创建的Cache实例

  • 第10行~第12行,如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去

至于如何判定上次查询和这次查询是一次查询?由于这里的CacheKey和MyBatis一级缓存使用的是同一个CacheKey,因此它的判定条件和前文写过的MyBatis一级缓存三个维度的判定条件是一致的。

最后再来谈一点,"Cache cache = ms.getCache()"这句代码十分重要,这意味着Cache是从MappedStatement中获取到的,而MappedStatement又和每一个标签所在的Mapper的Namespace+标签中定义的sql语句

对于MapperA来说,其中的任何一个条件都没有变化,自然会将原结果返回。

这个问题对于MyBatis的二级缓存来说是一个无解的问题,因此使用MyBatis二级缓存有一个前提:必须保证所有的增删改查都在同一个命名空间下才行

以上是【MyBatis源码解析】MyBatis一二级缓存的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn