Ir al contenido principal

Caches con Spring

Todo desarrollador se va a encontrar con el problema de hacer frente a un servicio cuyo funcionamiento sea especialmente lento. Existen varias razones diferentes que pueden hacer que un servicio particular tarde tiempo en dar respuesta:

  • Una elevada carga de peticiones en el servidor que deja escasa ram, y pocos ciclos de procesador para resolver la petición.
  • Una lógica de negocio muy complicada que requiera una gran cantidad de cálculos en cada petición.
  • Una base de datos remota alejada del servicio. Lo que en principio es casi un requerimiento de diseño (para tener aislados, securizados y a salvo los datos en un servidor propio), puede impactar bastante en los tiempos de resolución de un servicio. Toda petición a la base de datos debe pasar por la red para recuperar la información.

Los motivos son variados, y su solución no siempre está en la mano del desarrollo. Una optimización en los cálculos podría ayudar en determinados casos, pero una elevada carga del servidor, o un elevador trafico de red son impredecibles. Tampoco es una opción válida escalar servidores y tráficos de red para el peor de los casos...

A pesar de los diferentes motivos, buscar una solución es importante. Para sistemas online de interacción con el usuario, tener una respuesta casi inmediata es fundamental. Un retardo en las respuestas impacta de manera muy negativa en la productividad de los usuarios que utilizan el sistema, y es una de las principales causas de abandono en el uso de un software.

Las caches son una solución simple.

Sin grandes necesidades de análisis, ni complejos desarrollos de código; se puede conseguir una mejora extraordinaria en la velocidad de ejecución del código.

Hay que tener en cuenta que no siempre se podrá aplicar una cache a todos los servicios. Para poder hacer uso de una cache, los resultados de un servicio deben ser predecibles y estables. No podemos guardar en cache datos que dependen de factores aleatorios, y debemos tener localizados los lugares desde donde se modifican los datos de los servicios para poder actualizar las caches.

Mucho cuidado: Las caches son una optimización tardia en el desarrollo de software. Nunca se debería contemplar el uso de caches durante el desarrollo de un sistema. Las optimizaciones tempranas en el código son un antipatron, ya que nos alejan del diseño de la solución, y acaban introduciendo fuertes dependencias que complican el mantenimiento del código. También es el recurso sencillo de programadores perezosos, que en lugar de estudiar la forma de ejecutar un algoritmo de una forma eficiente, ya delegan el rendimiento a la cache en las primeras fases de diseño. Es fundamental que primero se diseñe una solución eficiente, capaz de funcionar de manera normal sin caches; y después de eso, analizar y diseñar la aplicación de caches para conseguir un plus de eficiencia. No nos engañemos, las caches no siempre van a poder aplicarse, y delegar en las caches la capacidad de que la aplicación funcione, incluso puede condenar la evolución del software en el futuro.

Caches con Spring-boot

Veamos un poco como utilizar caches en una aplicación con springboot y maven. Si utilizamos spring sin springboot, será necesario buscar las diferentes dependencias de forma más manual, pero el código resultante será el mismo. Simplemente añadimo spring-boot-stater-cache como dependencia maven para tener todas las librerías necesarias.

Spring proporciona una capa de abstracción para las caches, de tal forma que oculta al desarrollador el sistema de cache utilizado "por debajo". Ya usemos, ehcache, ignite, redis, o cache en memoria... Spring se hará cargo de toda la gestión de caches por nosotros en base a una serie de anotaciones.

Podemos partir de un controler rest básico, como el que tenemos aqui:

@RestController
@RequestMapping(value = "/users")
public class UserController {
    private final UserService userService;
    @Autowired
    UserController (UserService userService) {
        this.userService = userService;
    }
    @GetMapping(value = "/all")
    public List<User> getAllUsers() {
        return userService.findAll();
    }
}

Esa clase depende de un servicio userService, en cuya implementación forzaremos un cuello de botella.

@Service
public class UserService {
    private List<User> users = new ArrayList<>();
    @PostConstruct
    private void fillUsers() {
       users.add(User.builder().username("user_1").age(20).build());
       users.add(User.builder().username("user_2").age(76).build());
       users.add(User.builder().username("user_3").age(54).build());
       users.add(User.builder().username("user_4").age(30).build());
    }
    public List<User> findAll() {
        simulateSlowService();
        return this.users;
    }
    private void simulateSlowService() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Con este sencillo código tendremos un endpoint disponible en http://localhost:8080/user/all que tras tres segundos de espera entregará una lista de usuarios. La llamada al método getAllUsers del controlador, delegará en el servicio la recuperación de los usuarios. El servicio introduce un retardo artificial de tres segundos. Ese es un tiempo de respuesta inasumible para una aplicación en tiempo real...

Gracias a Spring y Spring boot es extremadamente sencillo hacer uso de las caches. Usando anotaciones de spring podemos marcar los métodos para que sus resultados se guarden en cache, y usando anotaciones de springboot podemos activar y configurar el uso de caches.

Con EnableCaching se activa el uso de caches en la aplicación.

@SpringBootApplication
@EnableCaching //enables Spring Caching functionality
public class SpringBootWithCachingApplication {
   public static void main(String[] args) {
      SpringApplication.run(SpringBootWithCachingApplication.class, args);
   }
}

Con CacheConfig y Cacheable se activan las caches en para los métodos de un servicio Spring.

@Service
@CacheConfig(cacheNames={"users"}) // tells Spring where to store cache for this class
public class UserService {
    private List<User> users = new ArrayList<>();
    @PostConstruct
    private void fillUsers() {
       users.add(User.builder().username("user_1").age(20).build());
       users.add(User.builder().username("user_2").age(76).build());
       users.add(User.builder().username("user_3").age(54).build());
       users.add(User.builder().username("user_4").age(30).build());
    }
    @Cacheable // caches the result of findAll() method
    public List<User> findAll() {
        simulateSlowService();
        return this.users;
    }
    private void simulateSlowService() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Con estas dos últimas anotaciones:

  • @CacheConfig: permite definifir características generales de las diferentes anotaciones de cache en un nivel genérico. En este caso, podemos marcar las caracterísitcas de las caches a nivel de clase, y dichas caracterísitcas se aplicaran a las diferentes anotaciones a nivel de método
  • @Cacheable:Cuando un método es anotado de esta forma, se guardará su resultado en una cache, y posteriores ejecuciones se recuperarn desde dicha cache, hasta que se la cache expire, o hasta que se vacie la cache.
Para que Spring pueda gestionar la cache basada en las anotaciones, la clase que use las anotaciones debe ser un servicio o componente de Spring, y debe ser utilizado desde el contexto de Spring. Si se utiliza el servicio directamente, nadie interpretará las anotaciones de cache.

Una vez modificado el código, podemos reiniciar la aplicación, y veremos los cambios. La primera llamada que se hace a http://localhost:8080/user/all ejecuturá el código completamente, y tardará tres segundos en completarse. Sin embargo posteriores ejecuciones del servicio recuperaran los datos de la cache, y se completaran en escasos milisegundos.

Refrescando las caches

Un problema que tiene el código anterior, es que una vez que los datos se guardan en la cache, siempre se recuperarn dichos datos. Si alguien modifica la lista de usuario, el resultado de la llamada a findAll no actualizará los resultados. Parece que no podremos ver los datos actualizados de los usuarios. Veamos cuales son los cambios necesarios para que cuando alguien realice algún cambio en la lista (crear, modificar o borrar), entonces podamos refrescar la cache.

Spring proporciona anotaciones para las diferentes operacioes, así que simplemente necesitamos anotar los métodos y dejar que spring se haga cargo de ello.

@CachePut
public User updateUser(User user) {
    this.users.set(0, user);
    return this.users.get(0);
}

Esta anotación:

  • @CachePut: siempre ejecuta el método, y se encarga de añadir a la cache el resultado del metodo (ojo: no su argumento).

También tenemos la anotación CacheEvict para quitar objetos de la cache. Dicha anotación se puede usar marcando con un allEntries para borrar toda la cache, o con un key para seleccionar un elemento a borrar.

@CacheEvict(allEntries = true) 

@CacheEvict(key = "#user.username")

Comentarios