Guava: Cache, un potente marco de almacenamiento en caché local

Guava Cache es un excelente marco de almacenamiento en caché local.

1. Configuración clásica

La estructura de datos de Guava Cache es similar a ConcurrentHashMap de JDK1.7: proporciona tres estrategias de reciclaje basadas en tiempo, capacidad y referencia, así como funciones como carga automática y estadísticas de acceso.

Configuracion basica

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

    }

En el ejemplo, la capacidad máxima de caché se establece en 100 ( reciclaje basado en la capacidad ) y se configuran la política de invalidación y la política de actualización .

1. Estrategia de fracaso

Cuando se configuran  expireAfterWrite , los elementos de la caché caducan dentro de un período de tiempo específico después de su creación o actualización por última vez.

2. Actualizar la estrategia

Configure  refreshAfterWrite el tiempo de actualización para que se puedan recargar nuevos valores cuando caduquen los elementos almacenados en caché.

En este ejemplo, algunos estudiantes pueden tener preguntas: ¿ Por qué necesitamos configurar la estrategia de actualización? ¿No es suficiente simplemente configurar la estrategia de invalidación ?

Por supuesto que es posible, pero en escenarios de alta concurrencia, configurar la estrategia de actualización será milagroso. A continuación, escribiremos un caso de prueba para facilitar que todos comprendan el modelo de subprocesos de Gauva Cache.

2. Comprender el modelo de hilo.

Simulamos la operación de "caducidad de la caché y ejecución del método de carga" y "actualización y ejecución del método de recarga" en un escenario de subprocesos múltiples.

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

 Los resultados de la ejecución se muestran en la siguiente figura.

Los resultados de la ejecución muestran que: Guava Cache no tiene un subproceso de tarea en segundo plano para ejecutar de forma asincrónica el método de carga o recarga.

  1. Estrategia de invalidación : expireAfterWrite permita que un subproceso ejecute el método de carga, mientras que otros subprocesos se bloquean y esperan.

    Cuando una gran cantidad de subprocesos obtienen valores almacenados en caché con la misma clave, solo un subproceso ingresará al método de carga, mientras que otros subprocesos esperan hasta que se genere el valor almacenado en caché. Esto también evita el riesgo de avería de la caché. En escenarios de alta concurrencia, esto aún bloqueará una gran cantidad de subprocesos.

  2. Estrategia de actualización : refreshAfterWrite permita que un subproceso ejecute el método de carga y que otros subprocesos devuelvan el valor anterior.

    En condiciones de concurrencia de una sola clave, el uso de refrescoAfterWrite no se bloqueará, pero si varias claves caducan al mismo tiempo, seguirá ejerciendo presión sobre la base de datos.

Para mejorar el rendimiento del sistema, podemos optimizar desde los dos aspectos siguientes:

  1. Configure la actualización <caducar para reducir la probabilidad de bloquear una gran cantidad de subprocesos;

  2. Adopte una estrategia de actualización asincrónica , es decir, el subproceso carga datos de forma asincrónica, durante la cual todas las solicitudes devuelven el valor de caché anterior para evitar una avalancha de caché.

La siguiente figura muestra el cronograma del plan de optimización:

3. Dos formas de implementar la actualización asincrónica

3.1 Anular el método de recarga

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 Implementar el método 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. Actualización asincrónica + caché multinivel

Escenas :

Una empresa de comercio electrónico necesita optimizar el rendimiento de la interfaz de la página de inicio de la aplicación. Al autor le tomó aproximadamente dos días completar toda la solución, utilizando un modo de caché de dos niveles y el mecanismo de actualización asincrónica de Guava.

La arquitectura general se muestra en la siguiente figura:

El proceso de lectura de caché es el siguiente :

1. Cuando se acaba de iniciar la puerta de enlace empresarial, no hay datos en el caché local. Lea el caché de Redis. Si no hay datos en el caché de Redis, llame al servicio de guía de compras a través de RPC para leer los datos y luego escriba el datos al caché local y Redis; si el caché de Redis no está vacío, los datos almacenados en caché se escribirán en el caché local.

2. Dado que el caché local se calentó en el paso 1, las solicitudes posteriores leen directamente el caché local y lo devuelven al usuario.

3. Guava está configurado con un mecanismo de actualización, que llamará al grupo de subprocesos LoadingCache personalizado (5 subprocesos máximo, 5 subprocesos principales) de vez en cuando para sincronizar los datos del servicio de guía de compras con el caché local y Redis.

Después de la optimización, el rendimiento es muy bueno, el consumo de tiempo promedio es de aproximadamente 5 ms y la frecuencia de aplicación de GC se reduce considerablemente.

Esta solución todavía tiene fallas. Una noche descubrimos que los datos que se mostraban en la página de inicio de la aplicación eran a veces los mismos y otras diferentes.

Es decir: aunque el hilo LoadingCache ha estado llamando a la interfaz para actualizar la información del caché, los datos en el caché local de cada servidor no son completamente consistentes.

Esto ilustra dos puntos muy importantes:

1. La carga diferida aún puede causar inconsistencia de datos en varias máquinas;

2. La cantidad de grupos de subprocesos de LoadingCache no está configurada de manera razonable, lo que genera una acumulación de tareas.

La solución sugerida es :

1. La actualización asincrónica combina el mecanismo de mensajes para actualizar los datos del caché, es decir: cuando cambia la configuración del servicio de guía de compras, se notifica a la puerta de enlace comercial para que vuelva a extraer los datos y actualice el caché.

2. Aumente adecuadamente los parámetros del grupo de subprocesos de LoadingCache y entierre puntos en el grupo de subprocesos para monitorear el uso del grupo de subprocesos. Cuando el subproceso está ocupado, se puede emitir una alarma y luego los parámetros del grupo de subprocesos se pueden modificar dinámicamente.

5. Resumen

Guava Cache es muy poderoso, no tiene un subproceso de tarea en segundo plano para ejecutar de forma asincrónica el método de carga o recarga, sino que realiza operaciones relacionadas a través de subprocesos de solicitud.

Para mejorar el rendimiento del sistema, podemos abordarlo desde los dos aspectos siguientes:

  1. Configure la actualización <caducar para reducir la probabilidad de bloquear una gran cantidad de subprocesos.

  2. Adopte una estrategia de actualización asincrónica , es decir, el subproceso carga datos de forma asincrónica, durante la cual todas las solicitudes devuelven el valor almacenado en caché anterior .

No obstante, todavía debemos considerar los problemas de coherencia de la caché y la base de datos al utilizar este enfoque. 

Supongo que te gusta

Origin blog.csdn.net/qq_63815371/article/details/135428100
Recomendado
Clasificación