Guava: Cache、強力なローカル キャッシュ フレームワーク

Guava Cache は、非常に優れたローカル キャッシュ フレームワークです。

1. クラシックな構成

Guava Cache のデータ構造は JDK1.7 の ConcurrentHashMap に似ており、時間、容量、参照に基づいた 3 つのリサイクル戦略と、自動読み込みやアクセス統計などの機能を提供します。

基本構成

    @Test
    public void testLoadingCache() throws ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println("加载 key:" + key);
                return "value";
            }
        };

        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为100(基于容量进行回收)
                .maximumSize(100)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        cache.put("Lasse", "穗爷");
        System.out.println(cache.size());
        System.out.println(cache.get("Lasse"));
        System.out.println(cache.getUnchecked("hello"));
        System.out.println(cache.size());

    }

この例では、最大キャッシュ容量が 100 (容量に基づいてリサイクル) に設定され、無効化ポリシー更新ポリシーが構成されています

1. 失敗戦略

構成されている 場合expireAfterWrite 、キャッシュ項目は、作成または最後に更新された後、指定された時間内に期限切れになります。

2. リフレッシュ戦略

refreshAfterWrite キャッシュされたアイテムの有効期限が切れたときに新しい値を再ロードできるように更新時間を構成します 。

この例では、「なぜ更新戦略を構成する必要があるのですか? 無効化戦略を構成するだけでは十分ではないのですか?」という疑問を持つ生徒もいるかもしれません。

もちろん可能ですが、同時実行性が高いシナリオでは、リフレッシュ戦略の構成は奇跡的になります。次に、Gauva Cache のスレッド モデルを誰もが理解しやすくするためにテスト ケースを作成します。

2. スレッドモデルを理解する

マルチスレッドシナリオで「キャッシュの有効期限とロードメソッドの実行」と「リフレッシュとリロードメソッドの実行」の両方の動作をシミュレートします。

@Test
    public void testLoadingCache2() throws InterruptedException, ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                Thread.sleep(500);
                return super.reload(key, oldValue);
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        System.out.println("测试过期加载 load------------------");

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        String hello = cache.get("hello");
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

        cache.put("hello2", "旧值");
        Thread.sleep(2000);
        System.out.println("测试重新加载 reload");
        //等待刷新,开始重新加载
        Thread.sleep(1500);
        ExecutorService executorService2 = Executors.newFixedThreadPool(5);
//        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 5; i++) {
            executorService2.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        //cyclicBarrier.await();
                        String hello = cache.get("hello2");
                        System.out.println(Thread.currentThread().getName() + ":" + hello);
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        Thread.sleep(9000);
    }

 実行結果は以下の図に示されています

実行結果は次のことを示しています。Guava Cache には、ロードまたはリロード メソッドを非同期的に実行するバックグラウンド タスク スレッドがありません。

  1. 無効化戦略: expireAfterWrite 1 つのスレッドがロード メソッドを実行できるようにし、他のスレッドはブロックして待機します。

    多数のスレッドが同じキーでキャッシュされた値を取得した場合、1 つのスレッドだけがloadメソッドに入り、他のスレッドはキャッシュされた値が生成されるまで待機します。これにより、キャッシュが破損するリスクも回避されます。同時実行性が高いシナリオでは、これによって依然として多数のスレッドがブロックされます。

  2. リフレッシュ戦略: refreshAfterWrite 1 つのスレッドがロード メソッドを実行できるようにし、他のスレッドが古い値を返すようにします。

    単一キーの同時実行では、refreshAfterWrite の使用はブロックされませんが、複数のキーが同時に期限切れになると、依然としてデータベースに負荷がかかります。

システムのパフォーマンスを向上させるには、次の 2 つの側面から最適化できます。

  1. 多数のスレッドがブロックされる可能性を減らすには、リフレッシュ < 期限切れを構成します。

  2. 非同期リフレッシュ戦略を採用します。つまり、スレッドはデータを非同期にロードします。その間、すべてのリクエストは古いキャッシュ値を返し、キャッシュ雪崩を防ぎます。

以下の図は、最適化計画のタイムラインを示しています。

3. 非同期リフレッシュを実装する 2 つの方法

3.1 リロードメソッドをオーバーライドする

ExecutorService executorService = Executors.newFixedThreadPool(5);
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                //从数据库加载
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
                    System.out.println(Thread.currentThread().getName() + "异步加载 key" + key);
                    return load(key);
                });
                executorService.submit(futureTask);
                return futureTask;
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

3.2 asyncReloading メソッドの実装

ExecutorService executorService = Executors.newFixedThreadPool(5);

        CacheLoader.asyncReloading(
                new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                        //从数据库加载
                        return "value_" + key.toLowerCase();
                    }
                }
                , executorService);

4. 非同期リフレッシュ + マルチレベルキャッシュ

シーン:

e コマース企業は、アプリのホームページ インターフェイスのパフォーマンスを最適化する必要があります。作者は、2 レベルのキャッシュ モードと Guava の非同期リフレッシュ メカニズムを使用して、ソリューション全体を完了するのに約 2 日かかりました。

全体的なアーキテクチャを次の図に示します。

キャッシュの読み取りプロセスは次のとおりです

1. ビジネスゲートウェイ起動直後はローカルキャッシュにデータがないため、Redisキャッシュを読み込みます Redisキャッシュにデータがない場合は、RPC経由でショッピングガイドサービスを呼び出してデータを読み込み、その後、データをローカル キャッシュと Redis に送信します。Redis キャッシュが空でない場合、キャッシュされたデータはローカル キャッシュに書き込まれます。

2. ローカル キャッシュは手順 1 でウォームアップされているため、後続のリクエストはローカル キャッシュを直接読み取り、ユーザーに返します。

3. Guava はリフレッシュ メカニズムを使用して構成されており、カスタム LoadingCache スレッド プール (最大 5 スレッド、5 コア スレッド) を時々呼び出して、ショッピング ガイド サービスからローカル キャッシュおよび Redis にデータを同期します。

最適化後のパフォーマンスは非常に良好で、平均消費時間は約 5 ミリ秒で、GC の適用頻度が大幅に減少しました。

このソリューションにはまだ欠陥があり、ある夜、アプリのホームページに表示されるデータが同じ場合もあれば異なる場合もあることがわかりました。

つまり、LoadingCache スレッドはキャッシュ情報を更新するためにインターフェイスを呼び出していますが、各サーバーのローカル キャッシュ内のデータは完全には一致していません。

これは 2 つの非常に重要な点を示しています。

1. 遅延読み込みにより、複数のマシン上でデータの不整合が発生する可能性があります。

2. LoadingCache スレッド プールの数が適切に構成されていないため、タスクが山積みになります。

推奨される解決策は次のとおりです

1. 非同期リフレッシュは、メッセージ メカニズムを組み合わせてキャッシュ データを更新します。つまり、ショッピング ガイド サービスの構成が変更されると、データを再プルしてキャッシュを更新するようにビジネス ゲートウェイに通知されます。

2. LoadingCache のスレッド プール パラメータを適切に増やし、スレッド プールの使用状況を監視するためのポイントをスレッド プールに埋め込み、スレッドがビジー状態の場合はアラームを発行し、スレッド プールのパラメータを動的に変更できます。

5. まとめ

Guava キャッシュは非常に強力で、ロードまたはリロード メソッドを非同期に実行するバックグラウンド タスク スレッドを持たず、代わりにリクエスト スレッドを通じて関連する操作を実行します。

システムのパフォーマンスを向上させるには、次の 2 つの側面から対処できます。

  1. 多数のスレッドがブロックされる可能性を減らすには、リフレッシュ < 期限切れを構成します。

  2. 非同期リフレッシュ戦略を採用します。つまり、スレッドはデータを非同期にロードします。その間、すべてのリクエストはキャッシュされた古い値を返します

それにもかかわらず、このアプローチを使用する場合は、キャッシュとデータベースの一貫性の問題を考慮する必要があります。 

おすすめ

転載: blog.csdn.net/qq_63815371/article/details/135428100