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 conempty()o!, porque el valor legítimo"0"también es falsy. - No serializar objetos complejos: Redis solo almacena strings. Serializa con
json_encodeoserializeantes 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 depconnect(). - KEYS en producción: es un comando O(n) que bloquea Redis. Usa siempre SCAN.
