Desarrollo backend

PostgreSQL + Redis: estrategias de caché para aplicaciones Laravel de alto rendimiento

Ruslan Ismailov Publicado 10 min de lectura
P

Introducción: por qué es necesario el caché y cuándo PostgreSQL deja de ser suficiente

PostgreSQL es uno de los sistemas de gestión de bases de datos relacionales más robustos y completos. Maneja perfectamente la carga transaccional, consultas JOIN complejas y grandes volúmenes de datos. Sin embargo, incluso la base de datos mejor configurada tiene sus límites: cuando se producen varios cientos de consultas analíticas pesadas por segundo, el tiempo de respuesta empieza a crecer y la utilización de CPU del servidor se acerca al 100%.

Síntomas típicos que indican que es hora de implementar caché:

  • El tiempo de ejecución de las consultas clave supera los 100–200 ms incluso con índices.

  • La misma consulta se ejecuta decenas de veces por segundo con los mismos parámetros.

  • El servidor PostgreSQL se convierte en un cuello de botella por I/O o CPU en momentos de pico.

  • Escalar la base de datos horizontalmente es difícil o demasiado costoso.

Redis junto con Laravel resuelve estos problemas de forma elegante: los datos consultados con frecuencia se almacenan en memoria RAM, y PostgreSQL solo responde a las solicitudes que no pueden ser atendidas desde el caché. Esta es la base fundamental de la arquitectura de aplicaciones PHP de alto rendimiento en 2026.

El rol arquitectónico de Redis junto a PostgreSQL: patrones de caché

Antes de escribir código, es importante elegir el patrón de caché adecuado. Cada uno tiene sus propios casos de uso y compromisos.

Cache-Aside (Lazy Loading)

El patrón más común en aplicaciones Laravel. La lógica es simple: la aplicación primero verifica el caché, y solo ante un fallo (cache miss) consulta PostgreSQL, guardando el resultado en Redis.

$products = Cache::remember('products.catalog.page.'.$page, 3600, function () use ($page) {
    return Product::with('category')
        ->where('is_active', true)
        ->orderBy('created_at', 'desc')
        ->paginate(20, ['*'], 'page', $page);
});

Ventajas: sencillez de implementación, solo se cachean los datos realmente solicitados.
Desventajas: la primera solicitud siempre va a la base de datos (cold start), posible obsolescencia temporal de los datos.

Write-Through

Con cada escritura, los datos se guardan simultáneamente en PostgreSQL y en Redis. Es adecuado para datos que se leen con mucha más frecuencia de lo que se escriben.

public function updateProduct(int $id, array $data): Product
{
    $product = Product::findOrFail($id);
    $product->update($data);
    
    // Actualizamos el caché de forma sincrónica
    Cache::put('product.'.$id, $product->fresh(), 3600);
    Cache::tags(['products'])->flush();
    
    return $product;
}

Read-Through

La lógica de caché se encapsula en una capa separada (repositorio o servicio), la aplicación siempre trabaja únicamente con el caché, y este consulta PostgreSQL de forma autónoma en caso de fallo. En Laravel, este patrón se implementa cómodamente mediante el Repository Pattern con un decorador.

Para la mayoría de los proyectos Laravel se recomienda comenzar con Cache-Aside como el enfoque más flexible, pasando a Write-Through para datos de referencia críticos.

Configuración de Redis en Laravel: ajustes, drivers, phpredis vs predis en 2026

Instalación y configuración básica

En 2026, para entornos de producción se recomienda sin duda la extensión phpredis en lugar del paquete predis. phpredis está implementado en C, funciona significativamente más rápido y consume menos memoria. Predis sigue siendo una opción solo cuando no es posible instalar extensiones PHP (por ejemplo, en algunos hostings administrados).

Instalación de phpredis:

# Ubuntu/Debian
apt install php-redis

# o mediante PECL
pecl install redis

En config/database.php configura la conexión:

'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),

    'default' => [
        'host'     => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port'     => env('REDIS_PORT', 6379),
        'database' => env('REDIS_DB', 0),
    ],

    'cache' => [
        'host'     => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port'     => env('REDIS_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1), // base de datos separada para caché
    ],
],

En .env:

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_CACHE_DB=1

Separación de bases de datos Redis

Es fundamental usar diferentes índices de base de datos Redis para distintos propósitos: caché, sesiones, colas. Esto simplifica la monitorización y permite limpiar únicamente el área necesaria sin afectar a los demás sistemas.

Caché de consultas a PostgreSQL mediante Laravel Cache: etiquetas, TTL e invalidación

Etiquetas de caché (Cache Tags)

Las Cache Tags son una herramienta potente para la invalidación grupal. En lugar de eliminar manualmente cada clave, basta con vaciar toda la etiqueta.

// Almacenamiento con etiquetas
$users = Cache::tags(['users', 'admin'])->remember(
    'users.admin.list',
    now()->addHour(),
    fn() => User::role('admin')->with('permissions')->get()
);

// Invalidación de toda la etiqueta al modificar un usuario
public function boot(): void
{
    User::saved(function (User $user) {
        Cache::tags(['users'])->flush();
    });
}

Importante: las Cache Tags solo funcionan con los drivers Redis y Memcached. No están disponibles con los drivers file o database.

Gestión flexible del TTL

Establece el TTL según la frecuencia de cambio de los datos:

  • Datos de referencia (categorías, configuraciones): 24 horas o más.

  • Catálogo de productos: 1–4 horas.

  • Estadísticas agregadas: 5–15 minutos.

  • Datos de usuario: 1–30 minutos según los requisitos de actualidad.

Invalidación mediante Model Events

La invalidación automática al modificar un modelo es una forma fiable de evitar datos obsoletos:

// app/Observers/ProductObserver.php
class ProductObserver
{
    public function saved(Product $product): void
    {
        Cache::tags(['products'])->flush();
        Cache::forget('product.'.$product->id);
    }

    public function deleted(Product $product): void
    {
        Cache::tags(['products'])->flush();
        Cache::forget('product.'.$product->id);
    }
}

// app/Providers/AppServiceProvider.php
Product::observe(ProductObserver::class);

Caché de sesiones y colas en Redis

Sesiones en Redis

Mover las sesiones del sistema de archivos o de PostgreSQL a Redis ofrece una mejora de rendimiento notable en escalado horizontal. Todos los nodos de la aplicación acceden a un almacén de sesiones centralizado sin necesidad de sincronizar archivos.

// config/session.php
'driver'     => env('SESSION_DRIVER', 'redis'),
'lifetime'   => env('SESSION_LIFETIME', 120),
'connection' => 'session', // conexión Redis separada

Colas con Redis

Redis Queue en Laravel proporciona un alto rendimiento para tareas en segundo plano. Comparado con el driver de colas basado en base de datos, Redis es órdenes de magnitud más rápido gracias a las operaciones atómicas sobre listas.

// .env
QUEUE_CONNECTION=redis

// despachar una tarea
ProcessOrderJob::dispatch($order)->onQueue('orders');

// iniciar workers
php artisan queue:work redis --queue=orders,default --tries=3 --timeout=60

Ejemplo práctico: caché de una consulta compleja a PostgreSQL con invalidación automática

Veamos un escenario real: un dashboard con estadísticas de ventas agregadas que incluye varios JOINs y window functions en PostgreSQL.

// app/Services/SalesDashboardService.php
class SalesDashboardService
{
    private const CACHE_TTL = 900; // 15 minutos
    private const CACHE_TAG = 'sales_dashboard';

    public function getSalesStats(int $userId, string $period): array
    {
        $cacheKey = "sales.stats.{$userId}.{$period}";

        return Cache::tags([self::CACHE_TAG, 'user.'.$userId])
            ->remember($cacheKey, self::CACHE_TTL, function () use ($userId, $period) {
                return DB::select(
                    "SELECT
                        DATE_TRUNC(:period, o.created_at) AS period_date,
                        COUNT(o.id) AS orders_count,
                        SUM(o.total_amount) AS revenue,
                        AVG(o.total_amount) AS avg_order_value,
                        RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS revenue_rank
                    FROM orders o
                    INNER JOIN order_items oi ON oi.order_id = o.id
                    WHERE o.user_id = :user_id
                        AND o.created_at >= NOW() - INTERVAL '1 year'
                        AND o.status = 'completed'
                    GROUP BY DATE_TRUNC(:period, o.created_at)
                    ORDER BY period_date DESC",
                    ['period' => $period, 'user_id' => $userId]
                );
            });
    }

    public function invalidateUserCache(int $userId): void
    {
        Cache::tags(['user.'.$userId])->flush();
    }
}

Invalidación al actualizar un pedido:

// app/Observers/OrderObserver.php
class OrderObserver
{
    public function __construct(
        private SalesDashboardService $dashboardService
    ) {}

    public function saved(Order $order): void
    {
        // Invalidamos solo el caché del usuario concreto
        $this->dashboardService->invalidateUserCache($order->user_id);
        
        // También vaciamos las estadísticas generales
        Cache::tags([SalesDashboardService::CACHE_TAG])->flush();
    }
}

Esta arquitectura permite reducir la carga sobre PostgreSQL en consultas repetidas al dashboard de ~150 ms a ~2 ms gracias al servicio desde Redis.

Monitorización de Redis y PostgreSQL: cómo identificar cuellos de botella

Monitorización de Redis

Métricas clave de Redis que hay que vigilar:

  • hit_rate (keyspace_hits / (keyspace_hits + keyspace_misses)): debe ser superior al 80–90% en un sistema estabilizado.

  • used_memory: no permitas que supere maxmemory, de lo contrario Redis comenzará a expulsar claves según la política de eviction.

  • connected_clients: un crecimiento anómalo puede indicar una fuga de conexiones.

  • latency: tiempo medio de ejecución de comandos; lo normal es < 1 ms.

# Monitorización mediante redis-cli
redis-cli INFO stats | grep keyspace
redis-cli INFO memory | grep used_memory_human
redis-cli MONITOR  # precaución: alta carga sobre el propio Redis
redis-cli --latency -h 127.0.0.1

Para producción se recomienda integrar con Prometheus + Redis Exporter o usar Laravel Telescope para rastrear cache hits/misses a nivel de aplicación.

Monitorización de PostgreSQL

Al trabajar junto con Redis, es importante verificar que el caché realmente reduce la carga sobre PostgreSQL:

-- Consultas más lentas (requiere pg_stat_statements)
SELECT query,
       calls,
       mean_exec_time,
       total_exec_time,
       rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Conexiones activas
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;

Si tras implementar el caché el valor de calls para las consultas pesadas no ha disminuido, significa que la invalidación ocurre con demasiada frecuencia o que las claves se están generando incorrectamente.

Laravel Telescope y Debugbar

En modo de desarrollo, Laravel Telescope ofrece información detallada sobre cada acceso a Redis y PostgreSQL: qué claves se leen, cuántos cache misses se producen y el tiempo de ejecución de las consultas. Es una herramienta imprescindible para depurar la estrategia de caché.

Conclusión: cuándo el caché ayuda y cuándo perjudica

El caché mediante Redis es una herramienta poderosa, pero no una solución mágica. Es importante entender en qué situaciones aporta un beneficio real y en cuáles genera problemas adicionales.

El caché ayuda cuando:

  • Los datos se leen con mucha más frecuencia de lo que cambian.

  • Calcular u obtener datos de PostgreSQL lleva un tiempo notable.

  • Las mismas consultas se ejecutan repetidamente con los mismos parámetros.

  • Se necesita escalado horizontal sin aumentar la carga sobre el SGBD.

El caché perjudica cuando:

  • Los datos cambian muy frecuentemente y los requisitos de actualidad son altos: el caché se invalidará constantemente, generando sobrecarga.

  • La lógica de invalidación es compleja y confusa: existe un alto riesgo de mostrar datos obsoletos.

  • El volumen de datos a cachear es enorme con memoria Redis limitada: la eviction reducirá el hit rate.

  • Las consultas son siempre únicas (por ejemplo, filtros complejos con muchas combinaciones posibles): el caché casi nunca producirá un hit.

Una estrategia de caché correcta en aplicaciones Laravel se construye de forma iterativa: primero mide los cuellos de botella reales con pg_stat_statements y Laravel Telescope, luego cachea solo lo que realmente sobrecarga PostgreSQL, y diseña cuidadosamente la invalidación. La combinación PostgreSQL + Redis, bien aplicada, permite manejar una carga 10–50 veces superior a la capacidad del sistema sin caché.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →