[MyBatis ソースコード解析] MyBatis の 1 次キャッシュと 2 次キャッシュ

巴扎黑
リリース: 2017-06-26 10:00:34
オリジナル
1339 人が閲覧しました

MyBatis Cache

頻繁なデータベース操作は非常にパフォーマンスを消費することがわかっています (主な理由は、DB の場合、データはディスクに永続化されるため、クエリ操作は IO を実行する必要があり、IO 操作速度がメモリと比較されるためです)特に一部の同一のクエリ ステートメントの場合、クエリ結果を保存し、次回同じコンテンツをクエリするときにデータをメモリから直接取得できるため、シナリオによっては、クエリ効率が大幅に向上する場合があります。

MyBatis のキャッシュは 2 つのタイプに分かれています:

  1. 1 次キャッシュ 1 次キャッシュは、同じクエリに対して SqlSession レベルの キャッシュです。

  2. 2 番目のレベルのキャッシュは、Mapper レベルのキャッシュです。これは、Mapper ファイルの で定義されます。 タグの設定に応じて、複数の Mapper ファイルが 1 つのキャッシュを共有できます

  3. MyBatis の第 1 レベルと第 2 レベルのキャッシュを詳しく見てみましょう。

MyBatis 1 次キャッシュのワークフロー

次に、MyBatis 1 次キャッシュのワークフローを見てみましょう。前述したように、MyBatis の 1 次キャッシュは SqlSession レベルのキャッシュです。openSession() メソッドが完了するか、SqlSession の close メソッドがアクティブに呼び出されると、SqlSession がリサイクルされ、1 次キャッシュもリサイクルされます。同じ時間です。前の記事で述べたように、MyBatis では、selectOne メソッドと selectList メソッドの両方が、実行のために最終的に selectList メソッドに変換されるため、SqlSession の selectList メソッドの実装を見てください。 4 行目のコードでは、BaseExeccutor のクエリ メソッドに移動します。

 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 }
ログイン後にコピー
ログイン後にコピー
3 行目では、キャッシュ条件 CacheKey を構築します。これには、条件が前のクエリとどのように同じであるかという質問が含まれます。同じ条件は、前の部分 結果が返され、コードのこの部分は次の部分での分析のために残されます。

次に、4 行目のクエリ メソッドの実装を見てください。コードは CachingExecutor にあります:

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 行目から 16 行目のコードを無視して、17 行目のクエリ メソッドに進みます。コードは BaseExecutor 内にあります:

 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 }
ログイン後にコピー
ログイン後にコピー

看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又和每一个