La idea es mostrar como tener un cache en memoria con caffeine(una forma de hacer cache en spring de manera muy optima) funcional en spring boot, usando reactor(akka flux/mono) y que podamos definir diferentes ttl(time to live, o tiempo de vida es decir cuando dura el cache hasta que deje de ser valido y se borre) para diferentes caches en una aplicacion.
El cache es para mi una estrategia de optimizacion que es conveniente usar en varios casos por ejemplo:
- si tenemos una respuesta de un consumo de una api que es valida por 10 horas, pero la usamos cada hora podriamos hacer un cache que viva 9 horas pues asi no ahorramos la espera en cada uno de los consumos pues guardamos los resultados en una capa de memoria intermedia
- si vemos que nuestra aplicacion va consumir de manera intensiva otra aplicacion y no queremos que la otra api tenga que procesar esos niveles de carga(haciendole un dos no intencionado).
- si estamos preocupados por el tiempo que va tomar un proceso en nuestra aplicacion y es aceptable tener información no actualizada para disminuir el tiempo que toma hacer una operación.
el primer llamado a un servicio puede lucir asi
pero luego de que el servicio esta cacheado nos ahorramos el llamado al servicio usando la información del llamado anterior.
en una aplicacion podemos tener diferentes servicios con diferentes necesidades y por tanto también podemos necesitar que unos caches vivan mas o menos que otros. por ejemplo si tenemos un cache para una apikey que nos sirve por 5 minutos y otro servicio que lo cacheamos por que lo llamamos muy seguido y lo cacheamos 5 segundos. por eso en algunos casos hace sentido que tengamos diferentes tiempo de vida.
bueno para hacer la demo lo primero que vamos a hacer es ir a https://start.spring.io/ y vamos a crear un proyecto con gradle, kotlin, spring boot 2.5.5 group com.cacheejemplo, nombre cacheejemplo, packaging jar, y java 17.
agregamos la dependencia de spring reactive web nos queda algo asi.
los descargamos, descomprimimos y abrimos en mi caso con intellij ide.
para usar caffeine un gestor de cache muy bueno vamos a agregarlo en el archivo build.gradle.kts en dependencies.
implementation("com.github.ben-manes.caffeine:caffeine")
implementation( "org.springframework:spring-context-support")
Colocamos la anotacion @EnableCaching en el archivo CacheejemploApplication (para que spring nos ponga a funcionar el cache), ademas de la anotacion @ConfigurationPropertiesScan para que podamos definir nuestra configuracion para cada uno de los caches nos queda asi.
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
@SpringBootApplication
@EnableCaching
@ConfigurationPropertiesScan
class CacheejemploApplication
fun main(args: Array<String>) {
runApplication<CacheejemploApplication>(*args)
}
luego vamos a crear un archivo llamado CacheStockApi.kt donde vamos a definir nuestros cache consumiendo estoy usando una api que encontre en el internet que devuelve
lastUpdateAt por que me servia para validar manualmente que las
respuestas se estaban cacheando y no requeria autentificacion ni nada https://walltime.info/api.html
import com.cacheejmplo.demo.CacheStockApi.CacheStockApi.min1Cache
import com.cacheejmplo.demo.CacheStockApi.CacheStockApi.min5Cache
import org.springframework.cache.annotation.CacheConfig
import org.springframework.cache.annotation.Cacheable
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
@Service
@CacheConfig(cacheNames=[min5Cache,min1Cache])
class CacheStockApi {
val webClient = WebClient.create("https://s3.amazonaws.com")
@Cacheable(min5Cache)
fun getApiInfo5Min(): Mono<String> =
getApiInfo().cache();
@Cacheable(min1Cache)
fun getApiInfo1Min(): Mono<String> =
getApiInfo().cache();
private fun getApiInfo(): Mono<String> {
return webClient.get().uri("/data-production-walltime-info/production/dynamic/walltime-info.json?now=1528962473468.679.0000000000873")
.accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(String::class.java);
}
object CacheStockApi{
const val min5Cache="5min"
const val min1Cache="1min"
}
}
aqui resaltar poner la anotacion @CacheConfig arriba de la clase con los nombres de los diferentes caches que vamos a usar, luego poner en cada metodo que queremos cachear poner la anotacion @Cacheable con el nombre del cache para ese metodo, y luego .cache al Mono que nos retorna la respuesta(esto es un hack para lograr que el cache nos funcione con spring https://stackoverflow.com/questions/48156424/spring-webflux-and-cacheable-proper-way-of-caching-result-of-mono-flux-type/48156827#comment122359092_48156827
lo siguiente es entonces habilitar estas configuraciones para el cache para estos vamos a crear un archivo CacheConfig.kt con el siguiente contenido.
import com.github.benmanes.caffeine.cache.Caffeine
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.cache.CacheManager
import org.springframework.cache.support.SimpleCacheManager
import org.springframework.context.annotation.Bean
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
import com.github.benmanes.caffeine.cache.Ticker
import org.springframework.cache.caffeine.CaffeineCache
@ConstructorBinding
@ConfigurationProperties(prefix = "caching")
data class CacheConfig(
val specs: Map<String, CacheSpecification>
) {
private val logger: Logger = LoggerFactory.getLogger(CacheejemploApplication::class.java)
@Bean
fun cacheManager(ticker: Ticker): CacheManager {
val manager = SimpleCacheManager()
if (specs != null) {
val caches = specs.entries.stream()
.map { (key, value): Map.Entry<String, CacheSpecification> ->
buildCache(
key,
value,
ticker
)
}.collect(Collectors.toList())
manager.setCaches(caches)
}
return manager
}
private fun buildCache(name: String, cacheSpec: CacheSpecification, ticker: Ticker): CaffeineCache {
logger.info("Cache {} specified timeout of {} min, max of {}", name, cacheSpec.timeoutSeconds?:0, cacheSpec.maxEntries)
val caffeineBuilder: Caffeine<Any, Any> = Caffeine.newBuilder()
.expireAfterWrite(cacheSpec.timeoutSeconds, TimeUnit.SECONDS).maximumSize(cacheSpec.maxEntries)
.ticker(ticker)
return CaffeineCache(name, caffeineBuilder.build())
}
@Bean
fun ticker(): Ticker {
return Ticker.systemTicker()
}
}
data class CacheSpecification(
val timeoutSeconds: Long,
val maxEntries: Long
)
con esto lo que logramos es usar caffeine como cache manager y habilitar que le pasemos configuracion al cache para que cada nombre de cache tenga su tiempo de vida en este caso en segundos.
vamos entonces a definir las configuraciones de nuestro cache en el archivo src.main.resources.application.properties
spring.main.allow-bean-definition-overriding=true
caching.specs.5min.timeoutSeconds=300
caching.specs.5min.maxEntries=200
caching.specs.1min.timeoutSeconds=60
caching.specs.1min.maxEntries=200
la linea de bean definition overriding es importante para que la aplicacion no nos falle, luego definimos por cada nombre de cache de antes cuanto tiempo dura en segundo y cuantas entradas queremos que mantenga maximas. en este caso definimos 2 caches uno de 5 minutos(60 segundos * 5=300) y otro de 1 minuto.
ya con esto listo para poder probarlo vamos a crear un archivo en src.main.kotlin.cacheejemplo.demo como controlador rest para usar nuestro cache. creamos un archivo ControladorApi.kt y pasamos el siguiente contenido.
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class ControladorApi(val cacheStockApi: CacheStockApi) {
@GetMapping("/5min")
fun min5()=
cacheStockApi.getApiInfo5Min()
@GetMapping("/1min")
fun min1()=
cacheStockApi.getApiInfo1Min()
}
ya con esto podemos correr la aplicacion, probar nuestro cache y revisar que unas respuestas se guardan por 5 minutos y otras solo 1 minuto.
localhost:8080/5min
localhost:8080/1min
quiero recordar que para que el cache funcione debe hacerse un llamado desde otro clase, cuando se llama a un método cacheado desde la misma clase spring no lo coge(no lo cachea lo llama)y es como si el metodo no fuera cacheado. por eso es mejor crear una clase que solo se encargue de cachear para que siempre la podamos llamar desde afuera(desde otra clase). tambien recordar poner todas las anotaciones relacionadas al cache EnabledCaching, CacheConfig, Cacheable y el .cache al Flux/mono para que el cache funcione.
este mismo proceso en video https://youtu.be/GmCBTNpVDRo
referencias:
.cache() para el mono extraida de https://stackoverflow.com/questions/48156424/spring-webflux-and-cacheable-proper-way-of-caching-result-of-mono-flux-type/48156827#comment122359092_48156827 respondida por Oleh Dokuka.
y la configuracion base para el cache con multiples tiempos de vida extraida de https://stackoverflow.com/questions/49885064/is-it-possible-to-set-a-different-specification-per-cache-using-caffeine-in-spri/61096118#comment122304131_61096118 respondida por nokieng