Redis desde PHP con PhpRedis: set/get, expiración y patrones de caché

Redis es una base de datos en memoria que funciona como servicio independiente, lo que lo hace ideal para compartir caché entre varios servidores PHP, gestionar sesiones distribuidas o implementar colas. La extensión PhpRedis es la forma más directa de usarlo desde PHP: una extensión nativa en C, más rápida que las alternativas en PHP puro.

Instalar PhpRedis

# Ubuntu/Debian
sudo apt install php-redis

# Con PECL
pecl install redis

# Verificar
php -m | grep redis

Conectar al servidor Redis

<?php
$redis = new Redis();

// Conexión básica
$redis->connect('127.0.0.1', 6379);

// Con contraseña
$redis->auth('tu_contraseña');

// Seleccionar base de datos (0-15)
$redis->select(0);

// Conexión persistente (mejor rendimiento en PHP-FPM)
$redis->pconnect('127.0.0.1', 6379);

// Con timeout y retry
$redis->connect('127.0.0.1', 6379, 2.5);  // timeout 2.5 segundos

// Verificar conexión
try {
    $redis->ping();
    echo "Conectado a Redis";
} catch (RedisException $e) {
    echo "Error: " . $e->getMessage();
}

Operaciones básicas: SET y GET

<?php
// SET simple
$redis->set('clave', 'valor');
echo $redis->get('clave');  // valor

// SET con TTL (SETEX)
$redis->setex('sesion_abc123', 3600, json_encode(['user_id' => 42]));
// O con el flag EX:
$redis->set('temporal', 'dato', ['EX' => 300]);

// SET solo si no existe (SETNX)
$redis->setnx('lock_proceso', 1);
// O con opciones:
$redis->set('lock_proceso', 1, ['NX' => true, 'EX' => 30]);

// GET devuelve false si la clave no existe
$valor = $redis->get('clave_inexistente');
var_dump($valor);  // bool(false)

// TTL restante
echo $redis->ttl('sesion_abc123');  // segundos; -1 = sin expiración; -2 = no existe

// Eliminar
$redis->del('clave');
$redis->del(['clave1', 'clave2', 'clave3']);  // múltiples

// Comprobar si existe
if ($redis->exists('clave')) { ... }

Hashes: almacenar objetos

Los hashes de Redis son perfectos para almacenar objetos sin serializar todo el contenido:

<?php
// HSET: guardar un hash
$redis->hMSet('usuario:42', [
    'nombre'  => 'Ana García',
    'email'   => '[email protected]',
    'rol'     => 'admin',
    'activo'  => 1,
]);

// HGET: obtener un campo
echo $redis->hGet('usuario:42', 'nombre');  // Ana García

// HMGET: obtener varios campos
$campos = $redis->hMGet('usuario:42', ['nombre', 'email']);

// HGETALL: todos los campos
$usuario = $redis->hGetAll('usuario:42');
// ['nombre' => 'Ana García', 'email' => '[email protected]', ...]

// HINCRBY: incrementar un campo numérico
$redis->hIncrBy('usuario:42', 'puntos', 10);

// Expiración del hash completo
$redis->expire('usuario:42', 3600);

Listas: colas y stacks

<?php
// LPUSH: añadir al inicio (cola FIFO con RPUSH+LPOP)
$redis->rPush('cola_emails', json_encode(['a' => '[email protected]', 'asunto' => 'Bienvenida']));
$redis->rPush('cola_emails', json_encode(['a' => '[email protected]', 'asunto' => 'Factura']));

// LPOP: extraer del inicio
$tarea = json_decode($redis->lPop('cola_emails'), true);

// BLPOP: bloquear hasta que haya un elemento (para workers)
$item = $redis->blPop(['cola_emails'], 5);  // espera 5 segundos
if ($item) {
    [$lista, $dato] = $item;
    procesarEmail(json_decode($dato, true));
}

// Longitud de la lista
echo $redis->lLen('cola_emails');

// Obtener un rango sin extraer
$pending = $redis->lRange('cola_emails', 0, -1);  // todos los elementos

Patrón cache-aside con Redis

<?php
class RedisCache {
    public function __construct(private Redis $redis) {}

    public function recuerda(string $clave, int $ttl, callable $fn): mixed {
        $cached = $this->redis->get($clave);
        if ($cached !== false) {
            return json_decode($cached, true);
        }
        $valor = $fn();
        $this->redis->setex($clave, $ttl, json_encode($valor));
        return $valor;
    }

    public function invalidar(string ...$claves): void {
        $this->redis->del($claves);
    }
}

$cache = new RedisCache($redis);

// Cachear el resultado de una consulta SQL
$articulos = $cache->recuerda('articulos_portada', 300, function() use ($pdo) {
    return $pdo->query('SELECT id, titulo, slug FROM articulos WHERE portada = 1 ORDER BY fecha DESC LIMIT 5')
               ->fetchAll(PDO::FETCH_ASSOC);
});

// Invalidar al publicar un nuevo artículo
$cache->invalidar('articulos_portada');

Contadores con INCR

<?php
// Contador de visitas por artículo y día
$clave = 'visitas:articulo:42:' . date('Ymd');
$visitas = $redis->incr($clave);
$redis->expire($clave, 86400 * 7);  // mantener 7 días

// Rate limiting: máx 60 requests por minuto por IP
function rateLimitRedis(Redis $redis, string $ip, int $max = 60): bool {
    $clave = 'rl:' . md5($ip) . ':' . date('YmdHi');
    $total = $redis->incr($clave);
    if ($total === 1) {
        $redis->expire($clave, 60);
    }
    return $total <= $max;
}

if (!rateLimitRedis($redis, $_SERVER['REMOTE_ADDR'])) {
    http_response_code(429);
    exit;
}

SCAN en lugar de KEYS

En producción, nunca uses KEYS *: bloquea Redis mientras busca. Usa SCAN en su lugar:

<?php
// MAL — bloquea el servidor si hay miles de claves
$claves = $redis->keys('sesion:*');

// BIEN — SCAN iterativo, no bloquea
$cursor = null;
$patron = 'sesion:*';
$todas  = [];

do {
    [$cursor, $encontradas] = $redis->scan($cursor, ['match' => $patron, 'count' => 100]);
    $todas = array_merge($todas, $encontradas);
} while ($cursor != 0);

// Eliminar todas las sesiones expiradas (combinando SCAN + TTL)
foreach ($todas as $clave) {
    if ($redis->ttl($clave) === -1) {  // sin expiración
        $redis->expire($clave, 86400); // forzar expiración
    }
}

Pipeline: enviar múltiples comandos en un solo round-trip

<?php
// Sin pipeline: 5 round-trips a Redis
for ($i = 1; $i <= 5; $i++) {
    $redis->set("item:$i", "valor$i");
}

// Con pipeline: 1 round-trip
$redis->pipeline();
for ($i = 1; $i <= 5; $i++) {
    $redis->set("item:$i", "valor$i");
}
$redis->exec();

// O con multi (transacción atómica)
$redis->multi();
$redis->incr('contador');
$redis->setex('clave', 60, 'valor');
$resultados = $redis->exec();

Gestión de sesiones PHP con Redis

; php.ini
session.save_handler = redis
session.save_path    = "tcp://127.0.0.1:6379?auth=contraseña&database=1"

Con esto, session_start() almacena automáticamente las sesiones en Redis, compartidas entre todos los servidores del cluster.

Errores frecuentes

  • get() devuelve false, no null: comprueba siempre con === false, no con empty() o !, porque el valor legítimo "0" también es falsy.
  • No serializar objetos complejos: Redis solo almacena strings. Serializa con json_encode o serialize antes de guardar.
  • pconnect en PHP-FPM: las conexiones persistentes en PHP-FPM requieren cuidado con el selector de base de datos; siempre llama a select() después de pconnect().
  • KEYS en producción: es un comando O(n) que bloquea Redis. Usa siempre SCAN.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP