Skip to content

NOTE

El cliente Redis de Bun soporta las versiones 7.2 y superiores del servidor Redis.

Bun proporciona bindings nativos para trabajar con bases de datos Redis con una API moderna basada en Promesas. La interfaz está diseñada para ser simple y de alto rendimiento, con gestión de conexiones incorporada, respuestas completamente tipadas y soporte TLS.

ts
import { redis } from "bun";

// Establecer una clave
await redis.set("greeting", "Hello from Bun!");

// Obtener una clave
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"

// Incrementar un contador
await redis.set("counter", 0);
await redis.incr("counter");

// Verificar si existe una clave
const exists = await redis.exists("greeting");

// Eliminar una clave
await redis.del("greeting");

Primeros Pasos

Para usar el cliente Redis, primero necesitas crear una conexión:

ts
import { redis, RedisClient } from "bun";

// Usando el cliente predeterminado (lee la información de conexión del entorno)
// process.env.REDIS_URL se usa por defecto
await redis.set("hello", "world");
const result = await redis.get("hello");

// Crear un cliente personalizado
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

Por defecto, el cliente lee la información de conexión de las siguientes variables de entorno (en orden de precedencia):

  • REDIS_URL
  • VALKEY_URL
  • Si no están establecidas, el valor predeterminado es "redis://localhost:6379"

Ciclo de Vida de la Conexión

El cliente Redis maneja automáticamente las conexiones en segundo plano:

ts
// No se realiza ninguna conexión hasta que se ejecuta un comando
const client = new RedisClient();

// El primer comando inicia la conexión
await client.set("key", "value");

// La conexión permanece abierta para comandos posteriores
await client.get("key");

// Cerrar explícitamente la conexión cuando se termine
client.close();

También puedes controlar manualmente el ciclo de vida de la conexión:

ts
const client = new RedisClient();

// Conectar explícitamente
await client.connect();

// Ejecutar comandos
await client.set("key", "value");

// Desconectar cuando se termine
client.close();

Operaciones Básicas

Operaciones con Cadenas

ts
// Establecer una clave
await redis.set("user:1:name", "Alice");

// Obtener una clave
const name = await redis.get("user:1:name");

// Obtener una clave como Uint8Array
const buffer = await redis.getBuffer("user:1:name");

// Eliminar una clave
await redis.del("user:1:name");

// Verificar si existe una clave
const exists = await redis.exists("user:1:name");

// Establecer expiración (en segundos)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // expira en 1 hora

// Obtener tiempo de vida restante (en segundos)
const ttl = await redis.ttl("session:123");

Operaciones Numéricas

ts
// Establecer valor inicial
await redis.set("counter", "0");

// Incrementar en 1
await redis.incr("counter");

// Decrementar en 1
await redis.decr("counter");

Operaciones con Hash

ts
// Establecer múltiples campos en un hash
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);

// Obtener múltiples campos de un hash
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// Obtener un solo campo de un hash (devuelve el valor directamente, null si falta)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"

// Incrementar un campo numérico en un hash
await redis.hincrby("user:123", "visits", 1);

// Incrementar un campo flotante en un hash
await redis.hincrbyfloat("user:123", "score", 1.5);

Operaciones con Conjuntos

ts
// Agregar miembro a un conjunto
await redis.sadd("tags", "javascript");

// Eliminar miembro de un conjunto
await redis.srem("tags", "javascript");

// Verificar si existe un miembro en un conjunto
const isMember = await redis.sismember("tags", "javascript");

// Obtener todos los miembros de un conjunto
const allTags = await redis.smembers("tags");

// Obtener un miembro aleatorio
const randomTag = await redis.srandmember("tags");

// Extraer (eliminar y devolver) un miembro aleatorio
const poppedTag = await redis.spop("tags");

Pub/Sub

Bun proporciona bindings nativos para el protocolo Redis Pub/Sub. Nuevo en Bun 1.2.23

Uso Básico

Para comenzar a publicar mensajes, puedes configurar un publicador en publisher.ts:

typescript
import { RedisClient } from "bun";

const writer = new RedisClient("redis://localhost:6739");
await writer.connect();

writer.publish("general", "Hello everyone!");

writer.close();

En otro archivo, crea el suscriptor en subscriber.ts:

typescript
import { RedisClient } from "bun";

const listener = new RedisClient("redis://localhost:6739");
await listener.connect();

await listener.subscribe("general", (message, channel) => {
  console.log(`Recibido: ${message}`);
});

En una terminal, ejecuta tu suscriptor:

bash
bun run subscriber.ts

y, en otra, ejecuta tu publicador:

bash
bun run publisher.ts

NOTE

El modo de suscripción toma el control de la conexión `RedisClient`. Un cliente con suscripciones solo puede llamar a `RedisClient.prototype.subscribe()`. En otras palabras, las aplicaciones que necesitan enviar mensajes a Redis requieren una conexión separada, obtenible mediante `.duplicate()`:
ts
import { RedisClient } from "bun";

const redis = new RedisClient("redis://localhost:6379");
await redis.connect();
const subscriber = await redis.duplicate(); 

await subscriber.subscribe("foo", () => {});
await redis.set("bar", "baz");

Publicar

La publicación de mensajes se realiza mediante el método publish():

typescript
await client.publish(channelName, message);

Suscripciones

El RedisClient de Bun te permite suscribirte a canales mediante el método .subscribe():

typescript
await client.subscribe(channel, (message, channel) => {});

Puedes cancelar la suscripción mediante el método .unsubscribe():

typescript
await client.unsubscribe(); // Cancelar suscripción de todos los canales.
await client.unsubscribe(channel); // Cancelar suscripción de un canal específico.
await client.unsubscribe(channel, listener); // Cancelar suscripción de un oyente específico.

Uso Avanzado

Ejecución de Comandos y Pipeline

El cliente automáticamente pone en pipeline los comandos, mejorando el rendimiento al enviar múltiples comandos en un lote y procesar las respuestas a medida que llegan.

ts
// Los comandos se ponen en pipeline automáticamente por defecto
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);

Para deshabilitar el pipeline automático, puedes establecer la opción enableAutoPipelining en false:

ts
const client = new RedisClient("redis://localhost:6379", {
  enableAutoPipelining: false, 
});

Comandos Crudos

Cuando necesitas usar comandos que no tienen métodos de conveniencia, puedes usar el método send:

ts
// Ejecutar cualquier comando de Redis
const info = await redis.send("INFO", []);

// LPUSH a una lista
await redis.send("LPUSH", ["mylist", "value1", "value2"]);

// Obtener rango de lista
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

El método send te permite usar cualquier comando de Redis, incluso aquellos que no tienen métodos dedicados en el cliente. El primer argumento es el nombre del comando y el segundo argumento es un array de argumentos de cadena.

Eventos de Conexión

Puedes registrar manejadores para eventos de conexión:

ts
const client = new RedisClient();

// Se llama cuando se conecta exitosamente al servidor Redis
client.onconnect = () => {
  console.log("Conectado al servidor Redis");
};

// Se llama cuando se desconecta del servidor Redis
client.onclose = error => {
  console.error("Desconectado del servidor Redis:", error);
};

// Conectar/desconectar manualmente
await client.connect();
client.close();

Estado de la Conexión y Monitoreo

ts
// Verificar si está conectado
console.log(client.connected); // booleano que indica el estado de la conexión

// Verificar la cantidad de datos en búfer (en bytes)
console.log(client.bufferedAmount);

Conversión de Tipos

El cliente Redis maneja automáticamente la conversión de tipos para las respuestas de Redis:

  • Las respuestas enteras se devuelven como números de JavaScript
  • Las cadenas bulk se devuelven como cadenas de JavaScript
  • Las cadenas simples se devuelven como cadenas de JavaScript
  • Las cadenas bulk null se devuelven como null
  • Las respuestas de array se devuelven como arrays de JavaScript
  • Las respuestas de error lanzan errores de JavaScript con códigos de error apropiados
  • Las respuestas booleanas (RESP3) se devuelven como booleanos de JavaScript
  • Las respuestas de mapa (RESP3) se devuelven como objetos de JavaScript
  • Las respuestas de conjunto (RESP3) se devuelven como arrays de JavaScript

Manejo especial para comandos específicos:

  • EXISTS devuelve un booleano en lugar de un número (1 se convierte en true, 0 se convierte en false)
  • SISMEMBER devuelve un booleano (1 se convierte en true, 0 se convierte en false)

Los siguientes comandos deshabilitan el pipeline automático:

  • AUTH
  • INFO
  • QUIT
  • EXEC
  • MULTI
  • WATCH
  • SCRIPT
  • SELECT
  • CLUSTER
  • DISCARD
  • UNWATCH
  • PIPELINE
  • SUBSCRIBE
  • UNSUBSCRIBE
  • UNPSUBSCRIBE

Opciones de Conexión

Al crear un cliente, puedes pasar varias opciones para configurar la conexión:

ts
const client = new RedisClient("redis://localhost:6379", {
  // Tiempo de espera de conexión en milisegundos (predeterminado: 10000)
  connectionTimeout: 5000,

  // Tiempo de espera de inactividad en milisegundos (predeterminado: 0 = sin tiempo de espera)
  idleTimeout: 30000,

  // Si se debe reconectar automáticamente al desconectarse (predeterminado: true)
  autoReconnect: true,

  // Número máximo de intentos de reconexión (predeterminado: 10)
  maxRetries: 10,

  // Si se deben poner en cola los comandos cuando está desconectado (predeterminado: true)
  enableOfflineQueue: true,

  // Si se deben poner en pipeline automáticamente los comandos (predeterminado: true)
  enableAutoPipelining: true,

  // Opciones TLS (predeterminado: false)
  tls: true,
  // Alternativamente, proporciona configuración TLS personalizada:
  // tls: {
  //   rejectUnauthorized: true,
  //   ca: "path/to/ca.pem",
  //   cert: "path/to/cert.pem",
  //   key: "path/to/key.pem",
  // }
});

Comportamiento de Reconexión

Cuando se pierde una conexión, el cliente intenta automáticamente reconectar con retroceso exponencial:

  1. El cliente comienza con un pequeño retraso (50ms) y lo duplica con cada intento
  2. El retraso de reconexión está limitado a 2000ms (2 segundos)
  3. El cliente intenta reconectar hasta maxRetries veces (predeterminado: 10)
  4. Los comandos ejecutados durante la desconexión:
    • Se ponen en cola si enableOfflineQueue es true (predeterminado)
    • Se rechazan inmediatamente si enableOfflineQueue es false

Formatos de URL Soportados

El cliente Redis soporta varios formatos de URL:

ts
// URL estándar de Redis
new RedisClient("redis://localhost:6379");
new RedisClient("redis://localhost:6379");

// Con autenticación
new RedisClient("redis://username:password@localhost:6379");

// Con número de base de datos
new RedisClient("redis://localhost:6379/0");

// Conexiones TLS
new RedisClient("rediss://localhost:6379");
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");

// Conexiones de socket Unix
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");

// TLS sobre socket Unix
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");

Manejo de Errores

El cliente Redis lanza errores tipados para diferentes escenarios:

ts
try {
  await redis.get("non-existent-key");
} catch (error) {
  if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
    console.error("La conexión al servidor Redis fue cerrada");
  } else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
    console.error("Autenticación fallida");
  } else {
    console.error("Error inesperado:", error);
  }
}

Códigos de error comunes:

  • ERR_REDIS_CONNECTION_CLOSED - La conexión al servidor fue cerrada
  • ERR_REDIS_AUTHENTICATION_FAILED - Falló la autenticación con el servidor
  • ERR_REDIS_INVALID_RESPONSE - Se recibió una respuesta inválida del servidor

Ejemplos de Casos de Uso

Caché

ts
async function getUserWithCache(userId) {
  const cacheKey = `user:${userId}`;

  // Intentar obtener del caché primero
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }

  // No está en caché, obtener de la base de datos
  const user = await database.getUser(userId);

  // Almacenar en caché por 1 hora
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);

  return user;
}

Limitación de Tasa

ts
async function rateLimit(ip, limit = 100, windowSecs = 3600) {
  const key = `ratelimit:${ip}`;

  // Incrementar contador
  const count = await redis.incr(key);

  // Establecer expiración si esta es la primera solicitud en la ventana
  if (count === 1) {
    await redis.expire(key, windowSecs);
  }

  // Verificar si se excedió el límite
  return {
    limited: count > limit,
    remaining: Math.max(0, limit - count),
  };
}

Almacenamiento de Sesiones

ts
async function createSession(userId, data) {
  const sessionId = crypto.randomUUID();
  const key = `session:${sessionId}`;

  // Almacenar sesión con expiración
  await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
  await redis.expire(key, 86400); // 24 horas

  return sessionId;
}

async function getSession(sessionId) {
  const key = `session:${sessionId}`;

  // Obtener datos de sesión
  const exists = await redis.exists(key);
  if (!exists) return null;

  const [userId, created, data] = await redis.hmget(key, ["userId", "created", "data"]);

  return {
    userId: Number(userId),
    created: Number(created),
    data: JSON.parse(data),
  };
}

Notas de Implementación

El cliente Redis de Bun está implementado en Zig y usa el Protocolo de Serialización de Redis (RESP3). Gestiona las conexiones de manera eficiente y proporciona reconexión automática con retroceso exponencial.

El cliente soporta pipeline de comandos, lo que significa que se pueden enviar múltiples comandos sin esperar las respuestas de los comandos anteriores. Esto mejora significativamente el rendimiento al enviar múltiples comandos en sucesión.

Limitaciones y Planes Futuros

Limitaciones actuales del cliente Redis que planeamos abordar en versiones futuras:

  • Las transacciones (MULTI/EXEC) deben hacerse mediante comandos crudos por ahora

Funciones no soportadas:

  • Redis Sentinel
  • Redis Cluster

Bun por www.bunjs.com.cn editar