Módulo 5 · Lección 4 20 min

Caché — mejorar el rendimiento

Aprende a usar el sistema de caché de Laravel para reducir consultas a la base de datos, acelerar tu API y mejorar el rendimiento de tu aplicación.

¿Por qué usar caché?

Las aplicaciones web modernas pueden recibir cientos o miles de peticiones por segundo. Si cada petición ejecuta las mismas consultas complejas a la base de datos o llama a las mismas APIs externas, el servidor se sobrecargará rápidamente y los tiempos de respuesta se dispararán.

La caché es una capa de almacenamiento temporal que guarda los resultados de operaciones costosas. Cuando llega la siguiente petición que necesita los mismos datos, se los devuelve desde la caché en milisegundos, sin repetir el trabajo.

Casos de uso comunes:

  • Resultados de consultas SQL complejas o lentas.
  • Datos de configuración leídos frecuentemente.
  • Respuestas de APIs externas (clima, tipos de cambio, etc.).
  • Contadores y estadísticas que no necesitan ser exactos al momento.
  • Resultados de operaciones matemáticas o generación de reportes.

Configurar el driver de caché

La configuración está en config/cache.php y se controla desde .env:

# Driver de archivo (por defecto, bueno para desarrollo)
CACHE_DRIVER=file

# Redis (recomendado para producción)
CACHE_DRIVER=redis

# Base de datos (si no tienes Redis)
CACHE_DRIVER=database

# Solo en memoria, se pierde al terminar la petición (útil para tests)
CACHE_DRIVER=array

Para usar Redis:

composer require predis/predis
# .env
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Si usas el driver database, crea la tabla de caché:

php artisan cache:table
php artisan migrate

Operaciones básicas de caché

Guardar un valor

use Illuminate\Support\Facades\Cache;

// Guardar durante 10 minutos
Cache::put('clave', 'valor', now()->addMinutes(10));

// Guardar durante 60 segundos
Cache::put('configuracion', $config, 60);

// Guardar para siempre (hasta que se borre manualmente)
Cache::forever('ajustes_globales', $ajustes);

Recuperar un valor

$valor = Cache::get('clave');

// Con valor por defecto si no existe
$valor = Cache::get('clave', 'valor_por_defecto');

// Con un closure como valor por defecto
$valor = Cache::get('clave', function () {
    return DB::table('configuracion')->value('campo');
});

Comprobar si existe

if (Cache::has('clave')) {
    $valor = Cache::get('clave');
}

Eliminar de la caché

Cache::forget('clave');

// Eliminar toda la caché
Cache::flush();

El método remember

Cache::remember() es el método que más usarás en la práctica. Combina la comprobación, la carga y el almacenamiento en una sola llamada:

// Si 'productos_destacados' existe en caché, lo devuelve.
// Si no existe, ejecuta el closure, guarda el resultado 30 minutos y lo devuelve.
$productos = Cache::remember('productos_destacados', now()->addMinutes(30), function () {
    return Producto::with('categoria')
        ->where('destacado', true)
        ->orderBy('ventas', 'desc')
        ->take(10)
        ->get();
});

Para guardar para siempre:

$config = Cache::rememberForever('configuracion_app', function () {
    return Configuracion::all()->keyBy('clave');
});

Caché en controladores de API

Un patrón muy común es cachear las respuestas de los endpoints más consultados:

// app/Http/Controllers/Api/ProductoController.php

public function index(): JsonResponse
{
    $productos = Cache::remember('api.productos.todos', now()->addMinutes(15), function () {
        return Producto::with('categoria')->paginate(20);
    });

    return response()->json($productos);
}

public function show(int $id): JsonResponse
{
    $producto = Cache::remember("api.productos.{$id}", now()->addHour(), function () use ($id) {
        return Producto::with(['categoria', 'reviews'])->findOrFail($id);
    });

    return response()->json($producto);
}

Invalidar la caché cuando los datos cambian

El mayor reto de la caché es mantenerla actualizada. Cuando un producto se actualiza o elimina, debes invalidar las claves de caché correspondientes:

public function update(Request $request, Producto $producto): JsonResponse
{
    $producto->update($request->validated());

    // Invalidar la caché de este producto y la lista general
    Cache::forget("api.productos.{$producto->id}");
    Cache::forget('api.productos.todos');

    return response()->json($producto);
}

public function destroy(Producto $producto): JsonResponse
{
    $producto->delete();

    Cache::forget("api.productos.{$producto->id}");
    Cache::forget('api.productos.todos');

    return response()->json(null, 204);
}

Tags de caché

Los tags te permiten agrupar entradas de caché y borrarlas todas a la vez. Son muy útiles para invalidar grupos relacionados de claves. Solo están disponibles con los drivers redis, memcached y array:

// Guardar con tags
Cache::tags(['productos', 'api'])->put('productos_todos', $productos, 3600);
Cache::tags(['productos', 'destacados'])->put('productos_destacados', $destacados, 3600);

// Recuperar con tags
$productos = Cache::tags(['productos', 'api'])->get('productos_todos');

// Invalidar todo el tag 'productos' de una sola vez
Cache::tags(['productos'])->flush();

Esto simplifica enormemente la gestión de la caché cuando tienes muchas claves relacionadas.

Cache::atomic — Operaciones atómicas

Para contadores y operaciones que pueden ejecutarse concurrentemente:

// Incrementar un contador atómicamente
Cache::increment('visitas_producto_1');
Cache::increment('visitas_producto_1', 5); // Incrementar en 5

// Decrementar
Cache::decrement('stock_producto_1');

Caché con Observers

Una forma elegante de gestionar la invalidación de caché es usar Observers de Eloquent:

// app/Observers/ProductoObserver.php

namespace App\Observers;

use App\Models\Producto;
use Illuminate\Support\Facades\Cache;

class ProductoObserver
{
    public function saved(Producto $producto): void
    {
        Cache::forget("api.productos.{$producto->id}");
        Cache::forget('api.productos.todos');
        Cache::tags(['productos'])->flush();
    }

    public function deleted(Producto $producto): void
    {
        Cache::forget("api.productos.{$producto->id}");
        Cache::forget('api.productos.todos');
        Cache::tags(['productos'])->flush();
    }
}

Registra el observer en un Service Provider:

// app/Providers/AppServiceProvider.php

use App\Models\Producto;
use App\Observers\ProductoObserver;

public function boot(): void
{
    Producto::observe(ProductoObserver::class);
}

Con esto, cada vez que un producto cambie, la caché se invalidará automáticamente sin tener que recordarlo en cada controlador.

Comandos de Artisan para caché

# Limpiar toda la caché de la aplicación
php artisan cache:clear

# Limpiar la caché de configuración
php artisan config:clear

# Limpiar la caché de rutas
php artisan route:clear

# Limpiar la caché de vistas Blade
php artisan view:clear

# Cachear la configuración para producción (más rápido)
php artisan config:cache

# Cachear las rutas para producción
php artisan route:cache

La caché bien implementada puede reducir los tiempos de respuesta de tu aplicación en un 80-90% para los endpoints más consultados. La clave está en identificar qué datos cambian con poca frecuencia y cuánto tiempo puedes tolerar que estén desactualizados.

Quiz

Pon a prueba lo aprendido

Responde las preguntas para comprobar que has entendido los conceptos clave.

1. ¿Cuál es el propósito principal del sistema de caché en Laravel?

2. ¿Qué método de Laravel guarda un valor en caché solo si todavía no existe?

3. ¿Qué driver de caché se recomienda para producción por su rendimiento?