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é"); }
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?
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.
Gracias Bryan! Reitero. Muy buen artículo!