viernes, 20 de noviembre de 2020

Apache Camel: Uso de Cache con Caffeine

Hoy vamos a ver como mejorar el rendimiento de nuestros web services en Apache Camel. Para ello usaremos Caffeine Cache, una sencilla pero potente cache en memoria realizada en Java y basada en la cache de Guava.

La idea detras del uso del cache, será poder almacenar el resultado de operaciones costosas en memoría. De esta forma, en sucesivas llamadas podremos acceder a dicho resultado de forma rápida y agil evitando la operación más costosa. 

Empecemos como siempre indicando cuales son las librerías a incluir. Por un lado la propia dependencía para usar Caffeine y por otro el starter de Spring que nos provee de una configuración por defecto. 

<dependency>
  <groupId>org.apache.camel.springboot</groupId>
  <artifactId>camel-caffeine-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-caffeine</artifactId>
</dependency>

¿Como funciona? Debemos pensar en la cache como su fuera un ConcurrentMap. Es decir, iremos colocando objetos asociados a una clave determinada y a través de esa clave podremos recuperarlos. Por tanto para poder usarlo, usaremos acciones del tipo GET o PUT asociados a una clave. El cuerpo del mensaje será el objeto almacenado en la cache. 

Cuando realicemos alguna operación Caffeine nos indicará si dicha operación ha resultado exitosa y si hay algún resultado asociado a la misma. Esta información nos la dará a través de valores almacenados en la cabecera: CamelCaffeineActionSucceeded y CamelCaffeineActionHasResult. Caffeine también tiene una interface, denominada CaffeineConstants, con todas las constantes asociadas a sus operaciones o resultados que nos puede ayudar en el desarrollo. 

A continuación veremos el ejemplo para obtener un libro en base a su identificador. El identificador nos servirá como clave para almacenar el libro asociado en la cache. Primero comprobaremos si se encuentra en la cache (acción GET), en dicho caso devolveremos el valor asociado. En caso contrario realizaremos la operación de consulta a base de datos y almacenaremos el resultado en la cache (acción PUT) para futuras consultas. 

rest().get("cache/book/{id}")
.description("Details of an book by id").outType(Book.class)
.produces(MediaType.APPLICATION_JSON_VALUE).route()
.log("Cache : Select Book By Id: ${header.id}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.log("Has Result ${header.CamelCaffeineActionHasResult} ActionSucceeded ${header.CamelCaffeineActionSucceeded}")
.choice().when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.FALSE))
   .to("sql:{{sql.selectById}}")
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
.otherwise()
   .log("Cache is working");

Si hacemos una prueba, comprobaremos que los tiempos y el rendimiento con Caffeine son mejores. Si lo comparamos usando hey, aún siendo una operación muy básica vemos como los tiempos se reducen mucho:


Pero tenemos que tener en cuenta que pasaría si el resultado de esa operación costosa cambiase. En dicho caso la cache seguría funcionando y devolviendo un valor erróneo. Para evitar eso tenemos, tenemos dos opciones. Primera opción a través de la configuración, con la cual podremos modificar cuántos resultados y cuanto tiempo se mantendrán en memoría. 

O una segunda opción, que será directamente invalidar manualmente la cache de un determinado objeto. Por ejemplo, en el caso de que borremos un objeto, no queremos que esa cache siga existiendo y devuelva resultados erróneos. Esto podremos hacerlo a través de la acción INVALIDATE. 

rest().delete("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.route().to("sql:{{sql.delete}}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_INVALIDATE))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.setHeader(Exchange.CONTENT_TYPE, constant(MediaType.APPLICATION_JSON_VALUE))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200)).setBody(constant(null));

Por último vamos a ver la opción de como debería comportarse una aplicación cuando se actualiza el objeto cacheado. La solución fácil sería invalidar la cache asociada a la clave y volver a cachearlo cuando sea invocado de nuevo. Pero esta opción nos hará realizar una operación costosa más, de las necesarias. Si ya tenemos el objeto podríamos simplemente almacenarlo de nuevo en la cache y evitar esa futura operación costosa. Ejemplo con el método PUT. 

rest().put("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.type(Book.class).route()
.choice().when().simple("${header.id} < 1")
   .bean(BookSQLRouter.class, "negativeId")
.otherwise()
   .log("PUT : Body: ${body}")//${body[id]
   .to("sql:UPDATE BOOK SET NAME = :#${body.name}, AUTHOR = :#${body.author} WHERE ID = :#id")
   .process(exchange -> {
       final Book book = exchange.getIn().getBody(Book.class);
       book.setId(Integer.valueOf(exchange.getIn().getHeader("id").toString()));
    })
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
   .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
   .setBody(constant(null));

Como siempre podeis ver el código completo aquí

No hay comentarios:

Publicar un comentario