Implementando una caché en Spring Boot

En la arquitectura orientada a servicios, es requisito construir componentes rápidos y que consuman la menor cantidad de recursos posibles. Esto puede ser difícil de lograr si tenemos una aplicación que necesite consultar una base de datos o un servicio externo para obtener datos de forma dinámica, más si esos datos varían poco con el tiempo. Hace algunos años era normal implementar una caché mediante una clase singleton, para instanciar los datos una sola vez y tener control de cuando refrescarlos, este antipatrón ha quedado obsoleto y con el tiempo han salido mejores alternativas.

Smart caching es un patrón para desarrollar aplicaciones de forma sencilla utilizando caché, permitiéndonos construir microservicios escalables y eficientes. Vamos a ver como en Spring esto es especialmente útil gracias al uso de anotaciones.

Configuración

Se debe agregar la dependencia spring-boot-starter-cache.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Para habilitar la caché, se agrega la anotación @EnableCaching a cualquier archivo de configuración.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("robots");
    }
}

Anotaciones

@Cacheable

Define un método cacheable, debe de ser parametrizado con el nombre de la caché donde los resultados serán almacenados.

@Cacheable(value = "robots")
public List<Robot> listar() {
	return robotDAO.findAll();
}

@CacheEvict

Utilizado para limpiar la caché, el parámetro allEntries le indica al componente que se debe de vaciar por completo.

@CacheEvict(value = "robots", allEntries = true)
public Robot guardar(Robot robot) {
	return robotDAO.save(robot);
}

@CacheEvict(value = "robots", allEntries = true)
public void eliminar(int idRobot) {
	robotDAO.deleteById(idRobot);
}

@CachePut

Permite ingresar o actualizar un valor a la caché, a diferencia de @Cacheable, @CachePut siempre se ejecuta y el resultado es almacenado en el componente.

@CachePut(value = "robots")
public List<Robot> recolectarDatos() {
	List<Robot> robots = listar();
	robots.stream().forEach(r -> r.setDatos(webService.consultar(r.getId())));
	return robotDAO.saveAll(robots);
}

@CacheConfig

Se utiliza para indicar la caché que se va a utilizar a nivel de clase y omitir este atributo en las demás anotaciones.

@Service
@CacheConfig(cacheNames = "robots")
public class RobotServiceImpl implements RobotService {

	@Autowired
	private DatosWebService webService;
	
	@Override
	@Cacheable
	public String consultar(int idRobot) {
		return webService.consultar(idRobot);
	}
}

Nótese que en este último ejemplo el método cacheable tiene parámetro, esto permite que se almacenen en caché n cantidad de resultados posibles según el valor de entrada.

Otros casos prácticos

Tener dos o más cachés en una misma aplicación

Spring permite configurar más de una caché, incluso se puede definir cual va a ser el componente por defecto en nuestra aplicación utilizando la anotación @Primary.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
    	SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
                new ConcurrentMapCache("canales"),
                new ConcurrentMapCache("emisoras")));
        return cacheManager;
    }
    
    @Bean
    public CacheManager caffeineCacheManager() {
    	CaffeineCacheManager cacheManager = new CaffeineCacheManager("robots");
    	cacheManager.setCaffeine(Caffeine.newBuilder()
    			.initialCapacity(50)
    			.maximumSize(200)
                .expireAfterAccess(30, TimeUnit.MINUTES)
    			.weakKeys()
    			.recordStats());
        return cacheManager;
    }
}

Posteriormente se puede utilizar el atributo cacheManager de la anotación @CacheConfig, para establecer un tipo de caché alternativo en una clase en particular.

@CacheConfig(cacheNames = "robots", cacheManager = "caffeineCacheManager")

Definir un tiempo de refrescamiento de la caché

Esto depende del tipo de caché que se esté utilizando, algunas librerías como Guava y Caffeine permiten asignar este valor en sus builders, en otros casos hay que utilizar el calendarizador de tareas de Spring. Para utilizar el calendarizador se requiere definir la anotación @EnableScheduling en algún archivo de configuración, el método @Scheduled puede estar en un controlador, un componente o un servicio.

@Autowired 
private CacheManager cacheManager;

@Scheduled(fixedDelay = 60 * 60 * 1000, initialDelay = 500)
public void refrescarCache() {
	cacheManager.getCacheNames().stream().forEach(x -> cacheManager.getCache(x).clear());
}

En este anterior ejemplo se programa el refrescamiento de todas las cachés cada hora, para limpiar una caché en específico se puede combinar el calendarizador en conjunto con un método @CacheEvict.

@Scheduled(fixedDelay = 60 * 60 * 1000, initialDelay = 500)
@CacheEvict(value = "robots", allEntries = true)
public void refrescarCache() {
	System.out.println("Refrescando caché");
}

3 comentarios en “Implementando una caché en Spring Boot”

  1. Muy buen artículo! Solo me queda de duda si esta forma de configurar un cache, qué esta utilizando por detrás (o por defecto) para guardar los datos? Memoria RAM? Es caché volátil?

    1. Hola Gabriel, muchas gracias! Si notas en la configuración inicial, el cacheManager del primer ejemplo no es más que un simple ConcurrentHashMap, por lo que se usa RAM. Spring permite configurar proveedores de caché externos, en esta entrada se muestra la implementación de Caffeine pero hay muchos más y cada uno cuenta con su tipo de almacenamiento. Por ejemplo, hay otro proveedor muy popular que es el Ehcache, el cual permite el almacenamiento en disco.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *