Spring AOPはRedisキャッシュデータベースの検索ソースを実現します。


アプリケーションシーン
私たちはデータベースクエリの結果をRedisにキャッシュしたいです。このように2回目に同じ問い合わせをすると、直接にredisから結果を取り、データベースの読み書き回数を減らすことができます。
解決すべき問題
キャッシュのコードはどこに書きますか?業務ロジックコードと完全に分離する必要があります。
汚れを避けるにはどうすればいいですか?キャッシュから読み出したデータは、データベースのデータと一致しなければならない。
どのようにしてデータベースクエリの結果に一意の識別情報を生成しますか?すなわち、この識別子(RedisではKey)によって、一つのクエリー結果を一意に決定することができ、同じクエリ結果は、必ず同じkeyにマッピングされる。このようにしてこそ、キャッシュ内容の正確性が保証されます。
どうやってアンケート結果を逐次しますか?照会結果は、単一のエンティティオブジェクトであってもよく、Listであってもよい。
ソリューション
汚読を避ける
クエリーの結果をキャッシュしましたが、データベース内のデータが変化するとキャッシュの結果が使えなくなります。この保証を実現するために、関連表の更新照会を実行する前に、関連するキャッシュを有効期限を過ぎさせることができます。このようにして、次の照会時には、プログラムはデータベースから新しいデータを読み出して、redisにキャッシュします。問題が来ました。どのようなキャッシュが期限切れになるかどうやって分かりますか?Redisに対しては、Hash Setデータ構造を使用して、Hash Setに対応するテーブルを作ります。このテーブルでのクエリーはすべてSetの下に保存されます。このようにテーブルデータが変動した場合、直接Setを有効期限を切ればいいです。私たちは注文をカスタマイズして、データベースの検索方法にこの操作とどの表が関連しているかを説明します。このように期限切れの操作を実行する時に、どのSetが期限切れになるかを直接に注釈から知ることができます。
クエリーに固有の識別情報を生成します。
MyBatisに対して、SQL文字列を直接keyとして使うことができます。しかし、このようにMyBatisベースのブロックを作成しなければなりません。それによって、あなたのキャッシュコードはMyBatisと密接に結合されます。いつか耐久層の枠を変えたら、キャッシュコードが無駄に書かれますので、この案は完璧ではありません。
よく考えてみますと、2回の照会で呼び出されたクラス名と方法名とパラメータ値が同じなら、この2回の照会結果は必ず同じであることが確認できます。したがって、この3つの要素を1つの文字列に結合してkeyとすることができ、識別問題を解決しました。
プログレッシブクエリの結果
一番便利なプログレッシブ方式はJDKが持参するObject OutputStreamとObject InputStreamを使うことです。利点はほとんどのオブジェクトであり、Serializableインターフェースを実現すれば、同じコードでプログレッシブ化され、逆プログレッシブ化されることができます。しかし、欠点も致命的です。それは順序化の結果、容量が大きく、redisで大量のメモリが消費されます。じゃ私たちはJSONという選択しか残っていません。
JSONの長所は構造がコンパクトで、可読性が強いことであるが、前列化対象に対しては具体的なタイプパラメータ(Classオブジェクト)を提供しなければならず、Listオブジェクトであれば、ListとListの要素タイプの2つの情報を提供しなければならないことが玉に瑕である。このようにしてコードの複雑さが増す。しかし、これらの困難は克服できるので、私たちはやはりJSONをプログレッシブストレージ方式として選択します。
コードはどこに書いてありますか
間違いなく、このAOPが登場しました。私達の例では、長期化フレームワークはMyBatisを使用していますので、私達の任務はMapperインターフェースメソッドの呼び出しをブロックすることです。Arond(サラウンド通知)を通じて以下のロジックを作成します。
メソッドが呼び出される前に、クラス名、方法名、パラメータ値からKeyを生成します。
Keyを通じてRedisに問い合わせを開始する。
キャッシュがヒットした場合、キャッシュ結果は、方法として呼び出された戻り値として逆順序化され、プロキシ方法の呼び出しを阻止する。
キャッシュがヒットしていない場合は、プロキシ方法を実行し、クエリ結果を得て、プログレッシブ化し、現在のKeyでプログレッシブ結果をredisに入れる。
コードの実装
ブロックしたいのはMapperインターフェース方法ですので、cglibの代理ではなく、JDKの動的エージェントをspringに使用するように命令しなければなりません。そのためには、以下のような構成が必要です。

<!--  proxy-target-class false   JDK     -->
<!--  true   cglib -->
<!-- cglib         -->
<aop:aspectj-autoproxy proxy-target-class="false" />
次に、タイプパラメータを伝えるために、インターフェース方法に表記された2つの注釈を定義する。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {
  Class type();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisEvict {
  Class type();
}
注の使い方は以下の通りです。

//           (       ?             :          )     
@RedisCache(type = JobPostModel.class)
JobPostModel selectByPrimaryKey(Integer id);

//                
@RedisEvict(type = JobPostModel.class)
int deleteByPrimaryKey(Integer id);
AOPのコードは以下の通りです

@Aspect
@Component
public class RedisCacheAspect {
  public static final Logger infoLog = LogUtils.getInfoLogger();
  @Qualifier("redisTemplateForString")
  @Autowired
  StringRedisTemplate rt;
  /**
   *      ,     。      ,       ,      ;
   *       ,       ,          
   * @param jp
   * @return
   * @throws Throwable
   */
  @Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.select*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.get*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.find*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.search*(..))")
  public Object cache(ProceedingJoinPoint jp) throws Throwable {
    //     、      
    String clazzName = jp.getTarget().getClass().getName();
    String methodName = jp.getSignature().getName();
    Object[] args = jp.getArgs();

    //     ,        key
    String key = genKey(clazzName, methodName, args);
    if (infoLog.isDebugEnabled()) {
      infoLog.debug("  key:{}", key);
    }
    //         
    Method me = ((MethodSignature) jp.getSignature()).getMethod();
    //             
    Class modelType = me.getAnnotation(RedisCache.class).type();

    //   redis      
    String value = (String)rt.opsForHash().get(modelType.getName(), key);
    // result          
    Object result = null;
    if (null == value) {
      //      
      if (infoLog.isDebugEnabled()) {
        infoLog.debug("     ");
      }
      //          
      result = jp.proceed(args);

      //        
      String json = serialize(result);
      //          
      rt.opsForHash().put(modelType.getName(), key, json);
    } else {
      //     
      if (infoLog.isDebugEnabled()) {
        infoLog.debug("    , value = {}", value);
      }
      //              
      Class returnType = ((MethodSignature) jp.getSignature()).getReturnType();

      //            json
      result = deserialize(value, returnType, modelType);

      if (infoLog.isDebugEnabled()) {
        infoLog.debug("       = {}", result);
      }
    }
    return result;
  }
  /**
   *           ,        
   * @param jp
   * @return
   * @throws Throwable
   */
  @Around("execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.insert*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.update*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.delete*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.increase*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.decrease*(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.complaint(..))" +
      "|| execution(* com.fh.taolijie.dao.mapper.JobPostModelMapper.set*(..))")
  public Object evictCache(ProceedingJoinPoint jp) throws Throwable {
    //         
    Method me = ((MethodSignature) jp.getSignature()).getMethod();
    //             
    Class modelType = me.getAnnotation(RedisEvict.class).type();
    if (infoLog.isDebugEnabled()) {
      infoLog.debug("    :{}", modelType.getName());
    }
    //       
    rt.delete(modelType.getName());
    return jp.proceed(jp.getArgs());
  }
  /**
   *     、        key
   * @param clazzName
   * @param methodName
   * @param args     
   * @return
   */
  protected String genKey(String clazzName, String methodName, Object[] args) {
    StringBuilder sb = new StringBuilder(clazzName);
    sb.append(Constants.DELIMITER);
    sb.append(methodName);
    sb.append(Constants.DELIMITER);

    for (Object obj : args) {
      sb.append(obj.toString());
      sb.append(Constants.DELIMITER);
    }
    return sb.toString();
  }
  protected String serialize(Object target) {
    return JSON.toJSONString(target);
  }
  protected Object deserialize(String jsonString, Class clazz, Class modelType) {
    //         List  
    if (clazz.isAssignableFrom(List.class)) {
      return JSON.parseArray(jsonString, modelType);
    }
    //           
    return JSON.parseObject(jsonString, clazz);
  }
}
これでデータベースクエリキャッシュの実現が完了しました。
UPDATE:
Hash Setのために有効期限を設定したほうがいいです。キャッシュポリシーが間違っていても、期限が切れたらデータベースと同期しています。

//          
      rt.execute(new RedisCallback<Object>() {
        @Override
        public Object doInRedis(RedisConnection redisConn) throws DataAccessException {
          //             String     
          //                 
          StringRedisConnection conn = (StringRedisConnection) redisConn;
          //   hash     
          //      ,   hash       
          if (false == conn.exists(hashName) ){
            conn.hSet(hashName, key, json);
            conn.expire(hashName, Constants.HASH_EXPIRE_TIME);
          } else {
            conn.hSet(hashName, key, json);
          }
          return null;
        }
      });
締め括りをつける
本文はSpring AOPに関してRedisキャッシュデータベースのソースコードの紹介を実現します。興味のある方はこの駅の他の関連テーマを参照してください。ここでは皆様の応援に感謝します。