トラブルシューティング-MyBatis 1レベルキャッシュによるページングプラグインの無効化
14345 ワード
症状:カスタムMyBatisページングプラグインを使用して、ページングパラメータの異なる方法だけが短時間で異なるページングパラメータを使用してクエリーした結果と同じです.病因:カスタムMyBatisプラグインブロックターゲットはStatementHandlerであり、同じSqlSessionではStatementHandlerである.prepareの前にMyBatisの1レベルのキャッシュがヒットしたので、キャッシュの内容を直接返しました.治療法:カスタムMyBatisページングプラグインを書き換えてExecutorをブロックするか、新しいプラグインを追加してExecutorをブロックして1級キャッシュをクリアします.
これは私が最近1つのプロジェクトで調べた問題で、ここに記録して後で調べる準備をします.
まず、このプロジェクトは比較的ポピュラーなPageHelperプラグインを使用していませんが、自分で実現しました.本人のコードではないので、貼り付けません.ネットで検索すると似たようなものも多く、主な実現原理はMyBatisが提供する
MyBatisのキャッシュメカニズムについては、ネット上では多くの資料が詳しく説明されており、不明な場合は先に理解することができます.総じて、同じSqlSessionで同じsql MyBatisを実行するとキャッシュに直接戻ります.しかし、私たちが直面した問題については、最初は疑問を持っていましたが、2回実行する方法はページパラメータが異なるのに、どうして同じキャッシュに命中したのでしょうか.この疑問を持ってdebug MyBatisのソースコードを確認し、CacheKeyを生成する論理を確認します.まず、sqlを実行すると、4パラメータ
ここでキャッシュキーの生成方法を見ると,実際には
createCacheKeyメソッドの実装クラスを表示すると、
ここでCacheKeyは4つのパラメータで構成されており,簡単に言えば実行されるべきである. SQLコードのID( MyBatisが持つメモリページング境界( xmlのsql文( 実際に実行するパラメータ( .
ページングパラメータが異なるため、ここでは毎回送信される
問題の原因が分かりました.これから私たちはこの問題を解決します.考え方は4つあります.A:ネット上で流行しているPageHelperを参考に、私たちのページングプラグインをExecutorをブロックするように書き換え、CacheKeyを生成する前にSQLを組み立てます.B:ページングメソッドの実行時にdisable一級キャッシュ;C:直接PageHelperで置換する;D:
disableキャッシュを使用するには、キャッシュがいつ使用されるかを知る必要があります.ではdebugを続けてみると,4パラメータ数の
ここには
やっと私たちが探している方法を見つけました.ここでは
ここでは、
CacheKeyを呼び出すたびに同じであるため、
ここの
では、どうやってキャッシュしますか?
明らかに、
ソリューションについてお話しすると、前に残した3つの疑問を解決します.なぜ
MyBatisの2次キャッシュ
実際にこれらの問題を理解するには,MyBatisの2次キャッシュが何であるかを理解するだけでよい.ソースコードを見続けます.
ここではまず、
2次キャッシュを使用しないと判断した場合、パラメータを設定することによってこの修飾器を閉じることができ、従来実行されていた
前に残した3つの疑問に答えてみましょう.デフォルトの2次キャッシュはオンなので、2次キャッシュを使用していなくても、MyBatisはExecutorを作成するたびにCachingExecutorで実際のExecutorオブジェクトを装飾します.
これは私が最近1つのプロジェクトで調べた問題で、ここに記録して後で調べる準備をします.
まず、このプロジェクトは比較的ポピュラーなPageHelperプラグインを使用していませんが、自分で実現しました.本人のコードではないので、貼り付けません.ネットで検索すると似たようなものも多く、主な実現原理はMyBatisが提供する
@Intercepts
を用いてStatementHandler
類のprepare
法を遮断し、反射によってMappedStatement
とBoundSql
を得ることである.約束されたページングメソッドが実行されている場合(MappedStatement
のidにPage接尾辞が付いている)では、BoundSql
のsql
フィールドをページング機能付きsqlに変更します.ページングクエリを使用するには、ページングメソッドのパラメータにページングパラメータが必要であり、ページングメソッド名には所定のPage接尾辞が必要です.一見問題はありませんが、実際に使用する場合はMyBatisのレベルが遅いため同じSqlSessionの後続のページングメソッドで同じCacheKeyが生成され、キャッシュ内のコンテンツが直接返されます.ここで主な問題はブロックのタイミングで、ブロックはMyBatisがキャッシュを使用するかどうかを決定した後に発生します!!MyBatisのキャッシュメカニズムについては、ネット上では多くの資料が詳しく説明されており、不明な場合は先に理解することができます.総じて、同じSqlSessionで同じsql MyBatisを実行するとキャッシュに直接戻ります.しかし、私たちが直面した問題については、最初は疑問を持っていましたが、2回実行する方法はページパラメータが異なるのに、どうして同じキャッシュに命中したのでしょうか.この疑問を持ってdebug MyBatisのソースコードを確認し、CacheKeyを生成する論理を確認します.まず、sqlを実行すると、4パラメータ
CachingExecutor.query
に進みます(ネットで調べてみると、このクラスは2次キャッシュを処理するために使用されています.2次キャッシュを使用していないのに、なぜこのクラスを使用するのですか.ここでは懸念を残して、後で話します): @Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
ここでキャッシュキーの生成方法を見ると,実際には
delegate
が呼び出されたcreateCacheKey
の方法である.(このdelegate
は何ですか?後で話しましょう): @Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
createCacheKeyメソッドの実装クラスを表示すると、
CachingExecutor
とBaseExecutor
しかないので、ここではBaseExecutor.createCacheKey
を使用してキャッシュキーを生成します.@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List 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);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
ここでCacheKeyは4つのパラメータで構成されており,簡単に言えば実行されるべきである.
ms.getId()
)、rowBounds.getOffset()
とrowBounds.getLimit()
)、boundSql.getSql()
)、boundSql.getParameterMappings()
を取得し、parameterObject
をマッピングすることによって取得される).ページングパラメータが異なるため、ここでは毎回送信される
parameterObject
が異なるが、ページングsqlはStatementHandler
で組み立てられているため、xmlのsqlは対応するパラメータを書いてページングパラメータを受け入れていないため、boundSql.getParameterMappings()
は私たちのページングパラメータを含んでいない.異なるparameterObject
も異なるCacheKeyをもたらしていない.問題の原因が分かりました.これから私たちはこの問題を解決します.考え方は4つあります.A:ネット上で流行しているPageHelperを参考に、私たちのページングプラグインをExecutorをブロックするように書き換え、CacheKeyを生成する前にSQLを組み立てます.B:ページングメソッドの実行時にdisable一級キャッシュ;C:直接PageHelperで置換する;D:
where #{pageNum} = #{pageNum} and #{pageSize} = #{pageSize}
などのページングパラメータをxmlに書き込む.これにより、実行結果に影響を与えることなく、CacheKeyの値が異なることも保証されます.実際に問題を発見した後の一時的な解決策はこれです.最終的に改造コストを評価した後、B案を使用することにした.disableキャッシュを使用するには、キャッシュがいつ使用されるかを知る必要があります.ではdebugを続けてみると,4パラメータ数の
CachingExecutor.query
でCacheKeyが生成された後に内部6パラメータのquery
メソッドが呼び出されたことが分かった. @Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List list = (List) tcm.getObject(cache, key);
if (list == null) {
list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
ここには
if (cache != null)
の判断ロジックがあり、最初はここがキャッシュが有効なロジックだと思っていたが、debug後に発見されたのはそうではなく、ここに入るたびにcacheが取ったのはnullで、ここでcacheを使う可能性を排除している.(じゃあ、ここのcacheは何ですか.焦らないで、前の2つの問題と一緒に話してください.)こちらは最後にdelegate.query
、つまりBaseExecutor.query
を呼び出しました.@Override
public List 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 list;
try {
queryStack++;
list = resultHandler == null ? (List) 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;
}
やっと私たちが探している方法を見つけました.ここでは
localCache.getObject(key)
を通じてキャッシュを取得し、直接戻る場合は、データベースへのクエリー結果queryFromDatabase
を実行し、queryFromDatabase
を参照してください.private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
ここでは、
localCache.putObject
によって1レベルのキャッシュが格納される.CacheKeyを呼び出すたびに同じであるため、
BaseExecutor.query
でキャッシュ戻りが直接使用されます.では、私たちの遮断器はどこで有効になりますか?次に、doQuery
メソッドは、doQuery
のような特定のサブクラスのSimpleExecutor.doQuery
メソッドを呼び出します. @Override
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
ここの
handler.prepare
こそ、本当に私たちのページングブロッカーにブロックされる方法であり、このときのキャッシュの使用はとっくに決定されています.では、どうやってキャッシュしますか?
BaseExecutor.query
の方法を振り返ってみると、ここには重要な判断があります. if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
明らかに、
ms
のflushCacheRequired
をtrueに設定することで、キャッシュを強制的にクリアすることができます.ブロッキングExecutor.query
を追加し、反射強度を利用してflushCacheRequired
のプロパティを変更します.なお、ここでは4パラメータのqueryメソッドのみをブロックでき、6パラメータのqueryメソッドは内部呼び出しであるため、動的エージェントにはブロックできない.具体的なコードは以下の通りです.@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})})
public class PageLocalCacheDisableInterceptor implements Interceptor {
private static final String DEFAULT_PAGE_SQLID = ".*Page$";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
if (ms.getId().matches(DEFAULT_PAGE_SQLID)) {
Class> clazz = ms.getClass();
Field flushLocalCache = clazz.getDeclaredField("flushCacheRequired");
flushLocalCache.setAccessible(true);
flushLocalCache.set(ms, true);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//do nothing
}
}
ソリューションについてお話しすると、前に残した3つの疑問を解決します.
CachingExecutor
を使うのですか?CachingExecutor
のうちdelegate
は何ですか?CachingExecutor
のうちcache
は何ですか?MyBatisの2次キャッシュ
実際にこれらの問題を理解するには,MyBatisの2次キャッシュが何であるかを理解するだけでよい.ソースコードを見続けます.
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
delegate
はExecutorであり、CachingExecutor
構造関数に伝達されることが分かった.CachingExecutor
自体もExecutor
を実現したため、それは実際には設計モードの装飾者モードである.このとき、私たちはこの線に沿って、なぜMyBatisがExecutorをCachingExecutor
で装飾するのかを調査し続けることができます.構造関数の呼び出し元を見てみると、Configuration.newExecutor
という呼び出しが1つしかありません. 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;
}
ここではまず、
executorType
がexecutorであることに基づいて対応する実装クラスをインスタンス化するとともに、cacheEnabled
がtrueであるか否かに基づいてCachingExecutor
で装飾するか否かを決定する.このcacheEnabled
には初期値trueがあり、XMLConfigBuilder
が構成相を構成するときにtrueをデフォルト値に設定するので、デフォルトではCachingExecutor
が付きます. protected boolean cacheEnabled = true;
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
2次キャッシュを使用しないと判断した場合、パラメータを設定することによってこの修飾器を閉じることができ、従来実行されていた
CachingExecutor.query
が実行されず、代わりに自身のBaseExecutor.query
メソッドが実行され、呼び出しリンクを簡略化することができる.次のように設定します.
前に残した3つの疑問に答えてみましょう.
CachingExecutor
のうちdelegate
が装飾されたExecutorオブジェクトである.CachingExecutor
のcache
は二次キャッシュであり、MyBatisは二次キャッシュを優先的に使用し、二次キャッシュがなければ一次キャッシュを使用し、一次キャッシュもなければデータベースクエリに接続します.2次キャッシュはデフォルトでオンですが、人為的な構成で使用する必要があります.構成はありませんので、毎回nullです.