StackPractices
beginner Por Mathias Paulenko

Conectar a Redis

Cómo conectar a Redis en Python, JavaScript y Java — con AUTH, TLS, pooling y los tipos de datos que realmente usarás.

Visión General

Redis es un almacén de datos en memoria — las lecturas y escrituras se resuelven en microsegundos porque nada toca disco en el camino caliente. Conectar es fácil; conectar bien es donde la gente se quema: sin AUTH en un puerto expuesto, sin TLS en una red pública, sin pool, sin política de maxmemory. Aquí tienes la configuración que funciona en Python, JavaScript y Java, más los detalles de conexión que la mayoría de tutoriales se saltan.

Cuándo Usar

Usa este recurso cuando:

  • Añades una capa de caché delante de una base de datos para bajar latencia y carga — consulta cache-aside, el patrón más común
  • Guardas sesiones compartidas entre instancias de la app (los servicios sin estado no pueden mantenerlas en proceso)
  • Construyes limitadores de tasa, clasificaciones, feature flags o contadores en tiempo real
  • Pasas mensajes ligeros entre servicios con pub/sub o Streams

Redis no es la opción por defecto cuando los datos deben sobrevivir un reinicio por sí mismos, cuando el dataset no cabe en RAM o cuando necesitas consultas ad-hoc sobre relaciones — para eso usa PostgreSQL o MySQL, y mantén Redis al lado, no en su lugar.

Solución

Python

import os
import redis

# redis://default:password@host:6379/0 — usa rediss:// para TLS
r = redis.Redis.from_url(
    os.getenv("REDIS_URL", "redis://localhost:6379/0"),
    decode_responses=True,
    socket_timeout=5,
    socket_connect_timeout=5,
    max_connections=20
)

# Strings
r.set('user:1:name', 'Alice', ex=3600)
name = r.get('user:1:name')

# Lists
r.lpush('queue:tasks', 'task_1')
task = r.brpop('queue:tasks', timeout=5)

# Hashes
r.hset('user:1', mapping={'email': 'alice@example.com', 'role': 'admin'})
user = r.hgetall('user:1')

JavaScript

const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
    // las URLs rediss:// activan TLS automáticamente; añade tls: {} para certs propios
    maxRetriesPerRequest: 3,
    retryStrategy: (times) => Math.min(times * 200, 2000), // backoff, ms
    enableReadyCheck: true,
    lazyConnect: false
});

// Strings
await redis.set('user:1:name', 'Alice', 'EX', 3600);
const name = await redis.get('user:1:name');

// Lists
await redis.lpush('queue:tasks', 'task_1');
const task = await redis.brpop('queue:tasks', 5);

// Hashes
await redis.hset('user:1', 'email', 'alice@example.com', 'role', 'admin');
const user = await redis.hgetall('user:1');

Java

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Map;

public class RedisExample {
    // Para TLS usa una URI: new JedisPool("rediss://default:pass@host:6379")
    private final JedisPool pool = new JedisPool("localhost", 6379);

    public void stringOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.setex("user:1:name", 3600, "Alice");
            String name = jedis.get("user:1:name");
        }
    }

    public void listOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.lpush("queue:tasks", "task_1");
            var task = jedis.brpop(5, "queue:tasks");
        }
    }

    public void hashOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.hset("user:1", "email", "alice@example.com");
            jedis.hset("user:1", "role", "admin");
            Map<String, String> user = jedis.hgetAll("user:1");
        }
    }
}

Explicación

Los tres clientes hablan RESP, el protocolo de Redis, y cada uno mantiene las conexiones de forma distinta: redis.Redis gestiona un pool interno (max_connections), ioredis envía los comandos por un único socket con lógica de reconexión integrada, y JedisPool entrega una conexión bloqueante por getResource() — siempre dentro de try-with-resources o se fuga.

Dos cosas que los ejemplos de camino feliz esconden. Primero, las instancias en producción necesitan AUTH: desde Redis 6, las ACLs te dan usuarios con nombre (ACL SETUSER app on >secret ~app:* +@all), y la contraseña viaja en la URL — redis://default:secret@host:6379. Segundo, redis:// plano va sin cifrar — sobre cualquier cosa que no sea loopback quieres rediss:// (TLS). Los servicios gestionados como ElastiCache y Redis Cloud te entregan un endpoint rediss:// justo por eso.

flowchart diagram: App[Aplicación

Una vez conectado y cacheando, el siguiente paso son los patrones de caché con Redis — nomenclatura de claves, protección anti-estampida y estrategias de invalidación.

Lo que funciona

  1. Pon la cadena de conexión en una variable de entorno (REDIS_URL) — las credenciales no van en el código, y el formato URL funciona en los tres drivers
  2. Establece expiración explícita (EX, PX, EXAT o KEEPTTL al reescribir) en las claves de caché para evitar crecimiento descontrolado de memoria
  3. Usa decode_responses=True en Python — por defecto devuelve bytes, que rompe las comparaciones de strings en silencio
  4. Prefiere BRPOP/BLPOP para consumir colas — las lecturas bloqueantes ganan a los bucles de polling
  5. Agrupa comandos relacionados en un pipeline (.pipeline() en redis-py y Jedis, .pipeline()/.multi() en ioredis) para reducir round-trips

Errores Comunes

  1. Exponer Redis en una interfaz pública sin AUTH ni TLS — es el puerto por defecto más escaneado de internet
  2. Guardar objetos grandes (>1 MB) en claves individuales, con picos de latencia en cada lectura
  3. No manejar los fallos de conexión, causando errores en cascada cuando Redis cae — degrada a la base de datos, no te estrelles
  4. Olvidar los TTLs, llegando a OOM command not allowed cuando maxmemory se llena
  5. Usar KEYS en producción — escanea todas las claves en el hilo único y bloquea la instancia; usa SCAN en su lugar

Ejemplos Avanzados

Python con pipelines y transacciones

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Pipeline: agrupa varios comandos para reducir RTT
pipe = r.pipeline()
pipe.set('user:1:name', 'Alice', ex=3600)
pipe.set('user:1:email', 'alice@example.com', ex=3600)
pipe.hset('user:1', mapping={'name': 'Alice', 'email': 'alice@example.com'})
results = pipe.execute()
print(results)  # [True, True, 1]

# Transacción con MULTI/EXEC — atómica, nada se intercala
pipe = r.pipeline(transaction=True)
pipe.set('counter', 0)
pipe.incr('counter')
pipe.incr('counter')
results = pipe.execute()
print(results)  # [True, 1, 2]

JavaScript con sorted sets y pub/sub

const Redis = require('ioredis');
const redis = new Redis({ host: 'localhost', port: 6379 });

// Sorted sets: clasificaciones
await redis.zadd('leaderboard', 100, 'alice', 85, 'bob', 120, 'charlie');
const topPlayers = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['charlie', '120', 'alice', '100', 'bob', '85']

// Pub/Sub — el suscriptor necesita su propia conexión
const subscriber = new Redis({ host: 'localhost', port: 6379 });
const publisher = new Redis({ host: 'localhost', port: 6379 });

await subscriber.subscribe('notifications');
subscriber.on('message', (channel, message) => {
    console.log(`Recibido en ${channel}: ${message}`);
});

await publisher.publish('notifications', JSON.stringify({ event: 'user_joined', userId: 42 }));

Java con JedisPool y scripting Lua

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;

public class RedisAdvanced {
    private final JedisPool pool;

    public RedisAdvanced(String host, int port) {
        this.pool = new JedisPool(host, port);
    }

    public void rateLimit(String userId, int maxRequests, int windowSeconds) {
        try (Jedis jedis = pool.getResource()) {
            String key = "rate:" + userId;
            String luaScript =
                "local current = redis.call('INCR', KEYS[1]) " +
                "if current == 1 then " +
                "  redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
                "end " +
                "return tonumber(current) <= tonumber(ARGV[1])";

            Object allowed = jedis.eval(
                luaScript, 1, key,
                String.valueOf(maxRequests), String.valueOf(windowSeconds)
            );
            System.out.println("Petición permitida: " + allowed);
        }
    }

    public void pipelineExample() {
        try (Jedis jedis = pool.getResource()) {
            var pipe = jedis.pipelined();
            pipe.set("key1", "value1");
            pipe.set("key2", "value2");
            pipe.sync();
        }
    }
}

Python async con redis-py

import asyncio
import redis.asyncio as aioredis

async def main():
    r = aioredis.Redis(host='localhost', port=6379, decode_responses=True)

    # Operaciones async con strings
    await r.set('async:key', 'value', ex=60)
    value = await r.get('async:key')

    # Pipeline async
    pipe = r.pipeline()
    pipe.set('a', 1)
    pipe.set('b', 2)
    pipe.get('a')
    results = await pipe.execute()
    print(results)  # [True, True, '1']

    await r.aclose()

asyncio.run(main())

Variantes

TecnologíaDriverAsyncClústerNotas
Pythonredis-pySí (redis.asyncio)redis.cluster.RedisClusterCliente oficial — el soporte de clúster viene integrado desde la 4.x
Pythonvalkey-pySíSíPara Valkey, el fork de Redis 7.2 de la Linux Foundation
JavaScriptioredisSí (Promise)new Redis.Cluster()Recomendado para Node.js
JavaScriptnode-redis (paquete redis)Sí (Promise)createCluster()Cliente oficial de Redis
JavaJedisNoJedisClusterAPI bloqueante simple
JavaLettuceSí (reactivo)RedisClusterClientEl default de Spring Data Redis
Gogo-redisSíClusterClientCliente estándar de Go

Consejos de Producción

  1. Configura maxmemory y una política de evicción. Sin ellos, Redis crece hasta que el OOM killer lo elige. allkeys-lru es el default correcto para cachés; noeviction para lo que sea durable:
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
  1. Monitorea con INFO y SLOWLOG. INFO muestra memoria, clientes y tasa de aciertos — mira keyspace_hits contra keyspace_misses; SLOWLOG GET lista los comandos que superaron slowlog-log-slower-than microsegundos:
info = r.info()
print(f"Memoria usada: {info['used_memory_human']}")
print(f"Tasa de aciertos: {info['keyspace_hits'] / (info['keyspace_hits'] + info['keyspace_misses']):.1%}")
  1. Usa Sentinel para alta disponibilidad. Sentinel vigila el master y promueve una réplica cuando falla; los tres clientes tienen modos compatibles con Sentinel (redis.sentinel.Sentinel, la opción sentinels de ioredis, JedisSentinelPool).

  2. Agrupa con PIPELINE, MGET y MSET. Todo lo que pase de unos pocos comandos seguidos debería ir en un solo round-trip:

values = r.mget('key1', 'key2', 'key3')
r.mset({'key1': 'val1', 'key2': 'val2'})
  1. Usa HSCAN en lugar de HGETALL en hashes grandes. HGETALL copia el hash entero a memoria de golpe; HSCAN itera en lotes:
for field, value in r.hscan_iter('large_hash', count=100):
    print(field, value)
  1. Activa los flags lazyfree (Redis 4.0+). El borrado asíncrono evita picos de latencia cuando claves grandes expiran o se evictan:
# redis.conf
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
  1. Inspecciona el almacenamiento con OBJECT ENCODING. Redis alterna entre codificaciones compactas y completas por clave — revisarlas te dice si un tipo de dato ocupa más de lo que debería:
encoding = r.object('encoding', 'mykey')
print(f"Encoding: {encoding}")  # ej. listpack, hashtable, int

Solución de Problemas

ErrorQué significaCómo arreglarlo
ECONNREFUSEDNo hay nada escuchando en 6379, o un firewall/grupo de seguridad lo bloqueaComprueba que redis-server está arriba, el puerto abierto y el host cliente permitido
NOAUTH Authentication requiredLa instancia tiene contraseña y no la enviastePásala en la URL: redis://:password@host:6379
WRONGPASS invalid username-password pairCredenciales ACL incorrectas (Redis 6+)Revisa el usuario (default salvo personalización) y la contraseña
READONLY You can't write against a read only replicaEstás escribiendo en una réplicaApunta las escrituras al master, o mira si Sentinel acaba de hacer failover
OOM command not allowedmaxmemory alcanzado y la política es noevictionSube maxmemory o cambia a una política de evicción como allkeys-lru
MISCONF Redis is configured to save RDB snapshotsLa persistencia a disco falla (normalmente disco lleno o permisos)Libera espacio o arregla los permisos del directorio — las escrituras siguen bloqueadas hasta arreglarlo
WRONGTYPE Operation against a key holding the wrong kind of valueLa clave existe con otro tipoComprueba TYPE key — no puedes hacer GET a un hash ni HGETALL a un string
Connection timed outCaída de red o el servidor ocupado en un comando lentoMira SLOWLOG, sube socket_timeout y mantén los comandos en O(log n)

Ver también

  • Documentación de Redis — comandos, tipos de datos, ACLs y client-side caching
  • ioredis — clúster, Sentinel, TLS y opciones de reintento para Node.js
  • redis-py — pools de conexión, redis.asyncio y redis.cluster
  • Jedis — JedisPooled, Sentinel y modos clúster para Java

Preguntas frecuentes

¿Debería usar Redis como mi base de datos principal?

No por sí sola. Redis vive en memoria — incluso con snapshots RDB o persistencia AOF activada, está diseñado como capa rápida junto a una base de datos real, no como sistema de registro. Combínalo con PostgreSQL o MySQL para los datos duraderos.

¿Cómo manejo el modo clúster de Redis?

Usa clientes compatibles con clúster: redis.cluster.RedisCluster en redis-py 4.x (el viejo paquete redis-py-cluster está deprecado e integrado en el cliente oficial), new Redis.Cluster() en ioredis, y JedisCluster o el RedisClusterClient de Lettuce en Java. El clúster divide los datos entre nodos, así que los comandos multi-clave solo funcionan dentro del mismo hash tag ({user:1}:name).

¿Cuál es la diferencia entre EX y PX en Redis?

EX establece la expiración en segundos, PX en milisegundos — SET k v EX 60 equivale a SET k v PX 60000. EXAT/PXAT toman timestamps absolutos en lugar de relativos, y KEEPTTL preserva el TTL existente al sobrescribir una clave.

¿Cómo implemento un limitador de tasa con Redis?

Usa INCR con un EXPIRE de ventana — o el script Lua de Ejemplos Avanzados si lo necesitas atómico entre drivers:

def rate_limit(r, user_id, max_requests=100, window=60):
    key = f"rate:{user_id}"
    current = r.incr(key)
    if current == 1:
        r.expire(key, window)
    return current <= max_requests
¿Cuál es la diferencia entre MULTI/EXEC y PIPELINE?

MULTI/EXEC es una transacción — los comandos corren atómicos y nada se intercala. PIPELINE solo agrupa comandos para ahorrar round-trips; otros clientes pueden intercalarse entre ellos. Usa MULTI cuando importa la corrección (contadores, transferencias), PIPELINE cuando solo cargas datos rápido.

¿Cómo uso Redis Streams para colas de mensajes?

XADD añade entradas, XREAD/XREADGROUP las lee de vuelta — Streams te da semántica tipo Kafka-lite con grupos de consumidores, entradas pendientes y replay:

# Productor
r.xadd('events', {'type': 'order_created', 'order_id': 123})

# Consumidor
response = r.xread({'events': '0'}, block=5000, count=10)
for stream, messages in response:
    for msg_id, fields in messages:
        print(f"Evento {msg_id}: {fields}")
¿Cómo conecto con AUTH y TLS?

Pon ambos en la URL — rediss://user:password@host:6379 te da TLS y AUTH en una sola cadena. redis-py la lee con Redis.from_url(), ioredis la parsea en el constructor (añade tls: {} para certificados CA propios) y Jedis acepta la URI rediss:// en JedisPool/JedisPooled.