Cache simple de datos y con Ehcache en Java

Escrito por el .
java planeta-codigo programacion
Enlace permanente Comentarios

Java

En ocasiones es necesario usar una cache que contenga un número de elementos hasta un límite y que cuando se vayan añadiendo más se vayan borrando otros según algún criterio. En Java si no queremos añadir una nueva dependencia al proyecto con una librería especializada como Ehcache la clase LinkedHashMap sirve para aquellos casos de uso simples sin necesidad de dependencias adicionales.

Si el coste de obtener algunos datos es costoso ya sean porque hay que obtenerlos de una base de datos, mediante una operación de disco o red o hay que hacer algún cálculo sobre ellos en estos casos guardar los datos en una cache supondrá un aumento notable de rendimiento de mayor o menor medida según el coste de la operación que evita la cache. Usar una cache es viable si es posible usar datos no completamente actualizados y dedicar algo de espacio en memoria para la cache. Si en la mayor parte de las búsquedas que se hacen en la cache el elemento buscado está ya presente se considera un acierto o hit y si no está presente un fallo o miss, cuando mayor sea el número de aciertos en la cache mayor será el aumento rendimiento percibido.

Para usar la clase LinkedHashMap como estructura de datos para una cache simple basta que creemos una nueva clase que extienda de esta con una implementación personalizada para el método removeEldestEntry que devuelva true para eliminar entrada más antigua, un caso sería cuando en el mapa se alcance el límite de elementos a almacenar como máximo.

En el ejemplo se crea una cache que tiene como máximo 5 elementos y se insertan en ella 15, cuando se intenta insertar en elemento más de la capacidad máxima el elemento más viejo se elimina de modo que la cache siempre tiene como máximo 5 elementos. Si la cache va a ser accedida tanto para operaciones de lectura como de escritura desde múltiples threads hay que prevenir posibles problemas de concurrencia sincronizando su acceso con el método Collections.synchronizedMap.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.github.picodotdev.blogbitix.javacache;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class Main {

    public static void main(String[] args) throws Exception {
        {
            System.out.println("LinkedHashMap");
            Map<Integer, Integer> cache = Collections.synchronizedMap(new SimpleCache<Integer, Integer>(5));
            IntStream.rangeClosed(1, 15).forEach(i -> cache.put(i, i));

            System.out.println(cache);
        }

        ...
    }
}
Main-1.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.github.picodotdev.blogbitix.javacache;

import java.util.LinkedHashMap;
import java.util.Map;

public class SimpleCache<K,V> extends LinkedHashMap<K,V> {

    private int size;

    public SimpleCache() {
        this(100);
    }

    public SimpleCache(int size) {
        this.size = size;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > size;
    }
}
SimpleCache.java

Si son necesarias funcionalidades más avanzadas como que los elementos expiren pasado un tiempo y para mayores cantidades de datos guardar parte de la cache en disco con un límite de espacio ocupado una de las opciones más conocidas es Ehcache.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package io.github.picodotdev.blogbitix.javacache;

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class Main {

    public static void main(String[] args) throws Exception {
        ...

        {
            System.out.println();
            System.out.println("Ehcache");
            CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("cache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, Integer.class,
                    ResourcePoolsBuilder.newResourcePoolsBuilder().heap(5, EntryUnit.ENTRIES).build())
                    .withExpiry(Expirations.timeToLiveExpiration(Duration.of(5, TimeUnit.SECONDS)))
                    .build())
                .build(true);

            Cache<Integer, Integer> cache = cacheManager.getCache("cache", Integer.class, Integer.class);
            IntStream.rangeClosed(1, 15).forEach(i -> cache.put(i, i));

            Map<Integer, Integer> map1 = new HashMap<>();
            cache.forEach(e -> map1.put(e.getKey(), e.getValue()));
            System.out.println(map1);

            Thread.sleep(6000);

            Map<Integer, Integer> map2 = new HashMap<>();
            cache.forEach(e -> map2.put(e.getKey(), e.getValue()));
            System.out.println(map2);
        }
    }
}
Main-2.java

Cachear datos se puede hacer en varios puntos de una aplicación, si se trata de una aplicación web Varnish cachea el HTML, CSS y JavaScript además de los códigos de estado incluso nginx incorpora la funcionalidad de cache, la propia aplicación puede cachear ciertos datos con alguna de las formas expuestas en este artículo y las propias bases de datos pueden cachear en memoria ciertos datos para evitar acceder al sistema de ficheros o disco. En definitiva una cache usada de forma efectiva ayuda a mitigar la penalización de operaciones costosas de acceso a red o a disco comparado con el acceso a memoria.

Ejecución del ejemplo

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: