Guava : Cache, un puissant framework de mise en cache locale

Guava Cache est un très excellent framework de mise en cache locale.

1. Configuration classique

La structure des données de Guava Cache est similaire à celle de ConcurrentHashMap du JDK 1.7. Elle fournit trois stratégies de recyclage basées sur le temps, la capacité et la référence, ainsi que des fonctions telles que le chargement automatique et les statistiques d'accès.

configuration de base

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

    }

Dans l'exemple, la capacité maximale du cache est définie sur 100 ( recyclage basé sur la capacité ) et la politique d'invalidation et la politique d'actualisation sont configurées .

1. Stratégie d'échec

Une fois configurés  expireAfterWrite , les éléments du cache expirent dans un délai spécifié après leur création ou leur dernière mise à jour.

2. Actualiser la stratégie

Configurez  refreshAfterWrite l'heure d'actualisation afin que les nouvelles valeurs puissent être rechargées lorsque les éléments mis en cache expirent.

Dans cet exemple, certains étudiants peuvent se poser des questions : Pourquoi faut-il configurer la stratégie de rafraîchissement ? Ne suffit-il pas de configurer simplement la stratégie d'invalidation ?

Bien sûr, c'est possible, mais dans des scénarios à forte concurrence, configurer la stratégie de rafraîchissement sera miraculeux. Ensuite, nous rédigerons un scénario de test pour faciliter la compréhension par chacun du modèle de thread de Gauva Cache.

2. Comprendre le modèle de thread

Nous simulons le fonctionnement à la fois de « l'expiration du cache et l'exécution de la méthode de chargement » et de « l'actualisation et l'exécution de la méthode de rechargement » dans un scénario multithread.

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

 Les résultats de l'exécution sont présentés dans la figure ci-dessous

Les résultats de l'exécution montrent que : Guava Cache ne dispose pas de thread de tâche en arrière-plan pour exécuter de manière asynchrone la méthode de chargement ou de rechargement.

  1. Stratégie d'invalidation : expireAfterWrite autorisez un thread à exécuter la méthode de chargement, tandis que les autres threads bloquent et attendent.

    Lorsqu'un grand nombre de threads obtiennent des valeurs mises en cache avec la même clé, un seul thread entrera dans la méthode de chargement, tandis que les autres threads attendront que la valeur mise en cache soit générée. Cela évite également le risque de panne du cache. Dans les scénarios à forte concurrence, cela bloquera toujours un grand nombre de threads.

  2. Stratégie d'actualisation : refreshAfterWrite autorisez un thread à exécuter la méthode de chargement et les autres threads à renvoyer l'ancienne valeur.

    Dans le cas d'une simultanéité de clé unique, l'utilisation defreshAfterWrite ne bloquera pas, mais si plusieurs clés expirent en même temps, cela exercera toujours une pression sur la base de données.

Afin d'améliorer les performances du système, nous pouvons optimiser les deux aspects suivants :

  1. Configurez l'actualisation < expire pour réduire la probabilité de bloquer un grand nombre de threads ;

  2. Adoptez une stratégie d'actualisation asynchrone , c'est-à-dire que le thread charge les données de manière asynchrone, au cours de laquelle toutes les requêtes renvoient l'ancienne valeur du cache pour éviter une avalanche de cache.

La figure ci-dessous montre la chronologie du plan d'optimisation :

3. Deux façons de mettre en œuvre l'actualisation asynchrone

3.1 Remplacer la méthode de rechargement

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 Implémenter la méthode 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. Actualisation asynchrone + cache multi-niveaux

Scènes :

Une entreprise de commerce électronique doit optimiser les performances de l’interface de la page d’accueil de l’application. Il a fallu environ deux jours à l'auteur pour finaliser l'intégralité de la solution, en utilisant un mode cache à deux niveaux et le mécanisme d'actualisation asynchrone de Guava.

L'architecture globale est présentée dans la figure ci-dessous :

Le processus de lecture du cache est le suivant :

1. Lorsque la passerelle d'entreprise vient de démarrer, il n'y a aucune donnée dans le cache local. Lisez le cache Redis. S'il n'y a aucune donnée dans le cache Redis, appelez le service de guide d'achat via RPC pour lire les données, puis écrivez le données vers le cache local et Redis ; si le cache Redis n'est pas vide, les données mises en cache seront écrites dans le cache local.

2. Puisque le cache local a été réchauffé à l'étape 1, les requêtes suivantes lisent directement le cache local et le renvoient à l'utilisateur.

3. Guava est configuré avec un mécanisme d'actualisation, qui appellera le pool de threads LoadingCache personnalisé (5 threads maximum, 5 threads principaux) de temps en temps pour synchroniser les données du service de guide d'achat avec le cache local et Redis.

Après optimisation, les performances sont très bonnes, la consommation de temps moyenne est d'environ 5 ms et la fréquence d'application du GC est considérablement réduite.

Cette solution présente encore des défauts : une nuit, nous avons constaté que les données affichées sur la page d'accueil de l'application étaient tantôt les mêmes, tantôt différentes.

C'est-à-dire : bien que le thread LoadingCache ait appelé l'interface pour mettre à jour les informations du cache, les données dans le cache local de chaque serveur ne sont pas complètement cohérentes.

Cela illustre deux points très importants :

1. Un chargement paresseux peut toujours entraîner une incohérence des données sur plusieurs machines ;

2. Le nombre de pools de threads LoadingCache n'est pas configuré de manière raisonnable, ce qui entraîne un empilement de tâches.

La solution proposée est :

1. L'actualisation asynchrone combine le mécanisme de message pour mettre à jour les données du cache, c'est-à-dire que lorsque la configuration du service de guide d'achat change, la passerelle d'entreprise est informée de réextraire les données et de mettre à jour le cache.

2. Augmentez de manière appropriée les paramètres du pool de threads de LoadingCache et enterrez les points dans le pool de threads pour surveiller l'utilisation du pool de threads. Lorsque le thread est occupé, une alarme peut être émise, puis les paramètres du pool de threads peuvent être modifiés dynamiquement.

5. Résumé

Guava Cache est très puissant. Il ne dispose pas de thread de tâche en arrière-plan pour exécuter de manière asynchrone la méthode de chargement ou de rechargement. Au lieu de cela, il effectue les opérations associées via des threads de requête.

Afin d'améliorer les performances du système, nous pouvons le traiter sous les deux aspects suivants :

  1. Configurez l'actualisation < expire pour réduire la probabilité de bloquer un grand nombre de threads.

  2. Adoptez une stratégie d'actualisation asynchrone , c'est-à-dire que le thread charge les données de manière asynchrone, au cours de laquelle toutes les demandes renvoient l'ancienne valeur mise en cache .

Néanmoins, nous devons toujours prendre en compte les problèmes de cohérence du cache et de la base de données lors de l’utilisation de cette approche. 

Je suppose que tu aimes

Origine blog.csdn.net/qq_63815371/article/details/135428100
conseillé
Classement