En este artículo vamos a hablar de una librería para implementar una caché a nuestros proyectos que me ha parecido muy interesante y de fácil integración. Vamos a hablar de Caffeine.

Caffeine es una biblioteca de caché de Java conocida por su eficiencia. En su interior, Caffeine emplea la política Window TinyLfu (combina información de frecuencia y antigüedad), lo que proporciona una alta tasa de aciertos (la relación entre el número de aciertos de caché y el número total de accesos a datos) y un bajo consumo de memoria. Este algoritmo es una buena opción para cachés de propósito general.

Características

Caffeine ofrece las siguientes funciones opcionales:

Dependencias

Para agregar esta librería a nuestro proyecto, hay que agregar la dependencia a nuestro pom.xml:

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

Configuración

Hay dos maneras de configurar Caffeine en Spring. La primera es configurando las propiedades de la caché en el archivo de configuración de la aplicación. En nuestro caso, se trata del archivo application.properties, pero también podría ser el archivo .yml.

El siguiente ejemplo define las propiedades de dos regiones de caché: source y paymentmethod (es decir, orígenes y medios de pago). Cada caché tendrá una capacidad inicial de 20 entradas, con un máximo de 100 entradas, y las entradas se eliminarán automáticamente de la caché una hora después de la última lectura o escritura (también conocido como tiempo de vida TTL).

spring.cache.cache-names=SOURCE_INFO,PAYMENTMETHOD_INFO
spring.cache.caffeine.spec=initialCapacity=20,maximumSize=100,expireAfterAccess=1h

Ahora veamos el segundo enfoque. Caffeine también se puede configurar programáticamente. Comenzamos declarando un Bean de Caffeine que contiene la especificación de la caché, como se muestra a continuación.

@Configuration
@EnableCaching
@ComponentScan("com.caffeinepoc")
public class CacheConfig {

    public static final String SOURCE_INFO_CACHE = "SOURCE_INFO";


@Bean
protected CacheManager cacheManager() {
    List<CaffeineCache> caffeineCaches = new ArrayList<CaffeineCache>();
    //Expirará en 1 minuto con un size de 500 elementos
    caffeineCaches.add(buildCaffeineCache(SOURCE_INFO_CACHE,1L,TimeUnit.MINUTES,500L));


    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager.setCaches(caffeineCaches);
    return cacheManager;

}

/**
 * Nos permite construir múltiples cachés
 *
 * @param cacheName
 * @param ttl
 * @param ttlUnit
 * @param size
 * @return
 */
private static CaffeineCache buildCaffeineCache(String cacheName,long ttl, TimeUnit ttlUnit,long size) {
    return new CaffeineCache(cacheName,Caffeine.newBuilder().expireAfterWrite(ttl, ttlUnit).maximumSize(size).build() );
}

Notificación de desalojo

Caffeine proporciona un mecanismo para notificar cuando se elimina una entrada de la caché. Se pueden añadir listeners a la configuración de la caché. Hay dos tipos de oyentes:

  1. Listener de desalojo: se activa cuando se produce un desalojo (es decir, una eliminación debido a la política que se esté implementando). Esta operación se realiza de forma síncrona.
  2. Listener de eliminación: se activa como consecuencia de un desalojo o una invalidación (es decir, una eliminación manual realizada por el usuario). La operación se ejecuta de forma asíncrona mediante un ejecutor, donde el ejecutor predeterminado es ForkJoinPool.commonPool() y se puede sobrescribir mediante Caffeine.executor(Executor).

Ambos listener reciben un RemovalListener, que es una interfaz funcional.

void onRemoval​(@Nullable K key, @Nullable V value, RemovalCause cause)

La clave y el valor normalmente son de tipo objeto y RemovalCause es una enumeración con la causa específica (EXPLICIT, REPLACED, COLLECTED, EXPIRED, SIZE).

Cleanup

Caffeine no realiza la limpieza, ni expulsa valores automáticamente ni al instante tras su caducidad de forma predeterminada. En su lugar, realiza pequeñas tareas de mantenimiento después de las operaciones de escritura. En algunas ocasiones, después de las operaciones de lectura si las escrituras son poco frecuentes.

Si su caché se utiliza principalmente para lecturas y rara vez para escrituras, es posible delegar la tarea de limpieza a un hilo. Se puede especificar un programador para que solicite la eliminación de las entradas caducadas, independientemente de si hay actividad de caché en ese momento.

Caffeine caché cleanup

Estadísticas

Las estadísticas se pueden activar con recordStats en el constructor de Caffeine. El método Cache.stats() devuelve un objeto CacheStats, que proporciona estadísticas como:

Vamos a añadir un controlador para ver las estadísticas que se generan que pueden ser muy útiles para ajustar nuestros parámetros de configuración para aplicaciones con alta demanda:

@RestController
@RequestMapping("admin/cache")
@Validated
public class CacheController {
    private CacheManager cacheManager;

    public CacheController(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }
    @GetMapping(produces = APPLICATION_JSON_VALUE)
    public List<CacheInfo> getCacheInfo() {
        return cacheManager.getCacheNames()
            .stream()
            .map(this::getCacheInfo)
            .toList();
    }

    @GetMapping(path = "/evict/{cachename}", produces = TEXT_PLAIN_VALUE)
    public String evictCache(@NotBlank @Size(min = 4, max = 40) @PathVariable String cachename) {
        var cache = cacheManager.getCacheNames()
            .stream()
            .filter(name -> name.equals(cachename))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("Cache was not found."));

        cacheManager.getCache(cachename).clear();
        return String.format("Cache source %s has been cleared.",cachename);
    }

    @GetMapping(path = "/evict", produces = TEXT_PLAIN_VALUE)
    public String evictAllCaches() {
        cacheManager.getCacheNames()
            .forEach(name -> cacheManager.getCache(name).clear());

        return "All Cache sources have been cleared.";
    }

    private CacheInfo getCacheInfo(String cacheName) {
        Cache<Object, Object> nativeCache = (Cache)cacheManager.getCache(cacheName).getNativeCache();
        Set<Object> keys = nativeCache.asMap().keySet();
        CacheStats stats = nativeCache.stats();
        return new CacheInfo(cacheName, keys.size(), keys, stats.toString());
    }
    private record CacheInfo(String name, int size, Set<Object> keys, String stats) {}

Hacemos una prueba con Postman y vemos lo siguiente.

  1. Estado inicial:
Prueba postman estado inicial
  1. Introducimos 3 registros:
Prueba postman introducción registros
  1. Después de unos minutos, vemos lo siguiente:
Prueba postman resultados tras introducir los registros

Conclusiones

Caffeine es una librería de fácil integración y con múltiples posibilidades. Destaca por su rendimiento dentro de la JVM, superando a menudo a otras bibliotecas de caché en operaciones de lectura y escritura. Está diseñado para ser extremadamente rápido y eficiente, utilizando algoritmos sofisticados para maximizar la tasa de aciertos de la caché. Caffeine es ideal para aplicaciones que requieren caché en memoria con baja latencia y alto rendimiento.

Es cierto que a veces hay otras cachés, como Ehcache, que nos van a proporcionar más funciones como caché multinivel o múltiples instancias de administración de caché. Otro ejemplo sería la de Redis, que nos va a ofrecer una caché distribuida. Por tanto, hay que valorar cuál es la mejor opción y cuál se adapta mejor a nuestras necesidades. Debemos de tener en cuenta que Caffeine consume mucha memoria, así que eso es un factor a tener en cuenta.

Si quieres echar un vistazo la POC completa que hemos visto en este post, te dejamos aquí el enlace al GitHub. ¡Te leo en comentarios! 👇

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete