Cuando la caché hace más lenta una aplicación Laravel
La caché es lo primero a lo que se recurre y lo último que se mide. Cuatro patrones que añaden latencia en vez de quitarla, y la pregunta que conviene hacerse antes de ninguno.
Toda aplicación lenta acaba teniendo una caché. Es la intervención con la mejor historia: el trabajo es caro, así que hágalo una vez y guarde la respuesta. A menudo funciona, y la página que tardaba dos segundos tarda ahora doscientos milisegundos.
Lo bastante a menudo como para escribir sobre ello, no funciona. Estas son las formas que toma cuando va al revés.
Una llamada a caché por fila
La versión más común, y la más difícil de ver porque cada pieza suelta es correcta.
foreach ($orders as $order) {
$rate = Cache::remember("fx.{$order->currency}", 3600, fn () =>
$this->rates->for($order->currency)
);
}Cada llamada es un milisegundo. Hay cuatrocientos pedidos, así que la página se pasa cuatrocientos milisegundos preguntándole a un sistema rápido el mismo puñado de cosas una y otra vez. La consulta original que sustituyó se ejecutaba una vez y tardaba doce.
La caché no falló aquí. La metieron dentro de un bucle, lo que convirtió una operación cara en cientos de baratas y luego las sumó. Cachee la colección en lugar del elemento, o lea el conjunto distinto una vez antes de que empiece el bucle.
Cachear algo más barato que la consulta
Un select por clave primaria indexada sobre una tabla caliente está muy por debajo del milisegundo. Un viaje a Redis por la red es comparable, a veces más lento, y ahora es dueño de dos sistemas donde uno bastaba, más la invalidación.
Merece la pena decirlo sin rodeos porque es donde más esfuerzo se gasta para menos retorno. Antes de cachear nada, cronometre lo que está a punto de cachear. Si la respuesta es un número de milisegundos de una sola cifra, la caché casi con seguridad no es la mejora que busca.
La estampida
Una clave con un informe que tarda cuatro segundos en construirse, caducando a medianoche, en un sitio con tráfico a medianoche. Cada petición que llega en los siguientes cuatro segundos encuentra la clave vacía y empieza a construirla. Nadie sirve desde caché, la base de datos ejecuta la misma consulta pesada cien veces en paralelo, y la caída la causa la caché en lugar de evitarla.
Laravel trae la respuesta:
$report = Cache::lock('report:monthly', 10)->block(5, function () {
return Cache::remember('report:monthly', 3600, fn () => $this->build());
});Una petición reconstruye; las demás esperan y luego leen lo que escribió. La alternativa - y la mejor cuando se acepta algo de desfase - es no dejar que la clave caduque bajo tráfico en absoluto: refrésquela desde un comando programado y deje que los lectores encuentren siempre algo ahí.
Invalidación que cuesta más que las lecturas
Una caché vale lo que ahorra menos lo que cuesta mantenerla correcta. Ate una clave a las actualizaciones de un modelo y una importación masiva que toque cincuenta mil filas dispara cincuenta mil invalidaciones, cada una una escritura al almacén de caché. Las lecturas de la página ahorraban ocho milisegundos cada una. La importación tarda ahora cuatro minutos más.
La misma trampa viene con etiquetas demasiado amplias. Una etiqueta que cubre todas las claves de un inquilino es cómoda hasta que vaciarla tiene que recorrer un espacio de claves grande, momento en el que la invalidación es el camino lento y se ejecuta en cada escritura.
La pregunta que va primero
No qué deberíamos cachear sino por qué esto es lento.
Una caché por delante de un N+1 hace que el N+1 salga más barato de ejecutar, y el N+1 seguirá ahí, y seguirá ejecutándose, ahora con un segundo sistema implicado y un problema de corrección enganchado. Una caché por delante de un índice que falta es una manera de no añadir el índice. Ninguna de las dos cosas es ilegítima como parche, pero las dos deberían anotarse como tales, porque un parche que nadie anota es arquitectura al año siguiente.
Cuando la respuesta es de verdad que el trabajo es caro y correcto y tiene que ocurrir igualmente - una llamada a un tercero con límite de peticiones, un agregado sobre millones de filas, un documento renderizado - cachear es exactamente lo correcto. Ese es un conjunto de casos más estrecho de lo que sugiere el reflejo.
Qué medir
Tres números, los tres baratos de obtener y rara vez mirados:
Tasa de aciertos por prefijo de clave. Por debajo del ochenta por ciento y el coste de los fallos se está comiendo las ganancias. Por debajo del cincuenta está pagando por una caché que en la práctica no tiene.
Llamadas a caché por petición. Una es una caché. Cuarenta es un bucle del que aún no se ha dado cuenta.
La página con la caché desactivada. Si la diferencia es pequeña, la caché es complejidad sin retorno, y borrarla es una mejora de rendimiento en el único sentido que importa: el número de cosas que pueden salir mal.
A menudo la consulta de debajo era el problema desde el principio, y el N+1 es la forma habitual que adopta. Si es toda la ruta de la petición y no una consulta, el trabajo de rendimiento empieza por medir. La caché llega después, o no llega.
