トラブルシューティング-MyBatis 1レベルキャッシュによるページングプラグインの無効化

14345 ワード

症状:カスタムMyBatisページングプラグインを使用して、ページングパラメータの異なる方法だけが短時間で異なるページングパラメータを使用してクエリーした結果と同じです.病因:カスタムMyBatisプラグインブロックターゲットはStatementHandlerであり、同じSqlSessionではStatementHandlerである.prepareの前にMyBatisの1レベルのキャッシュがヒットしたので、キャッシュの内容を直接返しました.治療法:カスタムMyBatisページングプラグインを書き換えてExecutorをブロックするか、新しいプラグインを追加してExecutorをブロックして1級キャッシュをクリアします.
これは私が最近1つのプロジェクトで調べた問題で、ここに記録して後で調べる準備をします.
まず、このプロジェクトは比較的ポピュラーなPageHelperプラグインを使用していませんが、自分で実現しました.本人のコードではないので、貼り付けません.ネットで検索すると似たようなものも多く、主な実現原理はMyBatisが提供する@Interceptsを用いてStatementHandler類のprepare法を遮断し、反射によってMappedStatementBoundSqlを得ることである.約束されたページングメソッドが実行されている場合(MappedStatementのidにPage接尾辞が付いている)では、BoundSqlsqlフィールドをページング機能付き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メソッドの実装クラスを表示すると、CachingExecutorBaseExecutorしかないので、ここでは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つのパラメータで構成されており,簡単に言えば実行されるべきである.
  • SQLコードのID(ms.getId())、
  • MyBatisが持つメモリページング境界(rowBounds.getOffset()rowBounds.getLimit())、
  • xmlのsql文(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();
        }
    

    明らかに、msflushCacheRequiredを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つの疑問に答えてみましょう.
  • デフォルトの2次キャッシュはオンなので、2次キャッシュを使用していなくても、MyBatisはExecutorを作成するたびにCachingExecutorで実際のExecutorオブジェクトを装飾します.
  • CachingExecutorのうちdelegateが装飾されたExecutorオブジェクトである.
  • CachingExecutorcacheは二次キャッシュであり、MyBatisは二次キャッシュを優先的に使用し、二次キャッシュがなければ一次キャッシュを使用し、一次キャッシュもなければデータベースクエリに接続します.2次キャッシュはデフォルトでオンですが、人為的な構成で使用する必要があります.構成はありませんので、毎回nullです.