Guava: Cache, ein leistungsstarkes lokales Caching-Framework

Guava Cache ist ein hervorragendes lokales Caching-Framework.

1. Klassische Konfiguration

Die Datenstruktur von Guava Cache ähnelt der ConcurrentHashMap von JDK1.7 und bietet drei Recyclingstrategien basierend auf Zeit, Kapazität und Referenz sowie Funktionen wie automatisches Laden und Zugriffsstatistiken.

Basiseinstellung

    @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());

    }

Im Beispiel ist die maximale Cache-Kapazität auf 100 festgelegt ( Recycling basierend auf der Kapazität ) und die Invalidierungsrichtlinie und die Aktualisierungsrichtlinie sind konfiguriert .

1. Fehlerstrategie

Bei Konfiguration  expireAfterWrite verfallen Cache-Elemente innerhalb einer bestimmten Zeitspanne nach ihrer Erstellung oder letzten Aktualisierung.

2. Strategie aktualisieren

Konfigurieren Sie  refreshAfterWrite die Aktualisierungszeit so, dass neue Werte neu geladen werden können, wenn zwischengespeicherte Elemente ablaufen.

In diesem Beispiel haben einige Schüler möglicherweise Fragen: Warum müssen wir die Aktualisierungsstrategie konfigurieren? Reicht es nicht aus, nur die Invalidierungsstrategie zu konfigurieren ?

Natürlich ist es möglich, aber in Szenarien mit hoher Parallelität stellt die Konfiguration der Aktualisierungsstrategie ein Wunder dar. Als Nächstes schreiben wir einen Testfall, um allen das Verständnis des Thread-Modells von Gauva Cache zu erleichtern.

2. Verstehen Sie das Thread-Modell

Wir simulieren den Vorgang „Cache-Ablauf und Ausführung der Lademethode“ und „Aktualisierung und Ausführung der Neulademethode“ in einem Multithread-Szenario.

@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);
    }

 Die Ausführungsergebnisse sind in der folgenden Abbildung dargestellt

Die Ausführungsergebnisse zeigen Folgendes: Guava Cache verfügt nicht über einen Hintergrundaufgabenthread, um die Lade- oder Neulademethode asynchron auszuführen.

  1. Invalidierungsstrategie : expireAfterWrite Erlauben Sie einem Thread, die Lademethode auszuführen, während andere Threads blockieren und warten.

    Wenn eine große Anzahl von Threads den zwischengespeicherten Wert mit demselben Schlüssel erhält, tritt nur ein Thread in die Lademethode ein, während andere Threads warten, bis der zwischengespeicherte Wert generiert wird. Dadurch wird auch das Risiko eines Cache-Ausfalls vermieden. In Szenarien mit hoher Parallelität wird dadurch immer noch eine große Anzahl von Threads blockiert.

  2. Aktualisierungsstrategie : refreshAfterWrite Erlauben Sie einem Thread, die Lademethode auszuführen, und anderen Threads, den alten Wert zurückzugeben.

    Bei der Parallelität einzelner Schlüssel führt die Verwendung von „refreshAfterWrite“ nicht zu einer Blockierung. Wenn jedoch mehrere Schlüssel gleichzeitig ablaufen, wird die Datenbank dennoch unter Druck gesetzt.

Um die Systemleistung zu verbessern, können wir die folgenden zwei Aspekte optimieren:

  1. Konfigurieren Sie die Aktualisierung <Ablauf, um die Wahrscheinlichkeit zu verringern, dass eine große Anzahl von Threads blockiert wird.

  2. Übernehmen Sie eine asynchrone Aktualisierungsstrategie, d. h. der Thread lädt Daten asynchron, wobei alle Anforderungen den alten Cache-Wert zurückgeben, um eine Cache-Lawine zu verhindern.

Die folgende Abbildung zeigt den Zeitplan des Optimierungsplans:

3. Zwei Möglichkeiten zur Implementierung einer asynchronen Aktualisierung

3.1 Überschreiben Sie die Reload-Methode

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 Implementieren Sie die asyncReloading-Methode

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. Asynchrone Aktualisierung + mehrstufiger Cache

Szenen :

Ein E-Commerce-Unternehmen muss die Leistung der App-Homepage-Oberfläche optimieren. Der Autor benötigte etwa zwei Tage, um die gesamte Lösung mithilfe eines zweistufigen Cache-Modus und des asynchronen Aktualisierungsmechanismus von Guava fertigzustellen.

Die Gesamtarchitektur ist in der folgenden Abbildung dargestellt:

Der Cache-Lesevorgang ist wie folgt :

1. Wenn das Business Gateway gerade gestartet wird, befinden sich keine Daten im lokalen Cache. Lesen Sie den Redis-Cache. Wenn sich keine Daten im Redis-Cache befinden, rufen Sie den Einkaufsführerdienst über RPC auf, um die Daten zu lesen, und schreiben Sie sie dann Daten werden in den lokalen Cache und Redis geschrieben. Wenn der Redis-Cache nicht leer ist, werden die zwischengespeicherten Daten in den lokalen Cache geschrieben.

2. Da der lokale Cache in Schritt 1 aufgewärmt wurde, lesen nachfolgende Anforderungen direkt den lokalen Cache und geben ihn an den Benutzer zurück.

3. Guava ist mit einem Aktualisierungsmechanismus konfiguriert, der von Zeit zu Zeit den benutzerdefinierten LoadingCache-Thread-Pool (maximal 5 Threads, 5 Kern-Threads) aufruft, um Daten vom Einkaufsführerdienst mit dem lokalen Cache und Redis zu synchronisieren.

Nach der Optimierung ist die Leistung sehr gut, der durchschnittliche Zeitverbrauch beträgt etwa 5 ms und die Häufigkeit der GC-Anwendung wird erheblich reduziert.

Diese Lösung weist immer noch Mängel auf. Eines Nachts stellten wir fest, dass die auf der Startseite der App angezeigten Daten manchmal gleich und manchmal unterschiedlich waren.

Das heißt: Obwohl der LoadingCache-Thread die Schnittstelle aufgerufen hat, um die Cache-Informationen zu aktualisieren, sind die Daten im lokalen Cache jedes Servers nicht vollständig konsistent.

Dies verdeutlicht zwei sehr wichtige Punkte:

1. Lazy Loading kann immer noch zu Dateninkonsistenzen auf mehreren Computern führen;

2. Die Anzahl der LoadingCache-Thread-Pools ist nicht angemessen konfiguriert, was zu einer Anhäufung von Aufgaben führt.

Die vorgeschlagene Lösung ist :

1. Die asynchrone Aktualisierung kombiniert den Nachrichtenmechanismus zum Aktualisieren der Cache-Daten. Das heißt: Wenn sich die Konfiguration des Einkaufsführer-Dienstes ändert, wird das Business Gateway benachrichtigt, die Daten erneut abzurufen und den Cache zu aktualisieren.

2. Erhöhen Sie die Thread-Pool-Parameter von LoadingCache entsprechend und vergraben Sie Punkte im Thread-Pool, um die Nutzung des Thread-Pools zu überwachen. Wenn der Thread ausgelastet ist, kann ein Alarm ausgegeben werden, und dann können die Thread-Pool-Parameter dynamisch geändert werden.

5. Zusammenfassung

Guava Cache ist sehr leistungsstark. Es verfügt nicht über einen Hintergrund-Task-Thread zur asynchronen Ausführung der Lade- oder Neulademethode, sondern führt verwandte Vorgänge über Anforderungsthreads aus.

Um die Systemleistung zu verbessern, können wir die folgenden zwei Aspekte berücksichtigen:

  1. Konfigurieren Sie „Refresh < Expire“, um die Wahrscheinlichkeit zu verringern, dass eine große Anzahl von Threads blockiert wird.

  2. Übernehmen Sie eine asynchrone Aktualisierungsstrategie , d. h. der Thread lädt Daten asynchron, wobei alle Anforderungen den alten zwischengespeicherten Wert zurückgeben .

Dennoch müssen wir bei diesem Ansatz immer noch Probleme mit dem Cache und der Datenbankkonsistenz berücksichtigen. 

Ich denke du magst

Origin blog.csdn.net/qq_63815371/article/details/135428100
Empfohlen
Rangfolge