Skip to content

NOTE

Redis-клиент Bun поддерживает версии Redis-сервера 7.2 и выше.

Bun предоставляет нативные привязки для работы с базами данных Redis с современным Promise-based API. Интерфейс разработан быть простым и производительным, со встроенным управлением соединениями, полностью типизированными ответами и поддержкой TLS.

ts
import { redis } from "bun";

// Установить ключ
await redis.set("greeting", "Hello from Bun!");

// Получить ключ
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"

// Инкрементировать счётчик
await redis.set("counter", 0);
await redis.incr("counter");

// Проверить существование ключа
const exists = await redis.exists("greeting");

// Удалить ключ
await redis.del("greeting");

Начало работы

Для использования Redis-клиента сначала нужно создать соединение:

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

// Использование клиента по умолчанию (читает информацию о соединении из окружения)
// process.env.REDIS_URL используется по умолчанию
await redis.set("hello", "world");
const result = await redis.get("hello");

// Создание пользовательского клиента
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

По умолчанию клиент читает информацию о соединении из следующих переменных окружения (в порядке приоритета):

  • REDIS_URL
  • VALKEY_URL
  • Если не установлено, по умолчанию используется "redis://localhost:6379"

Жизненный цикл соединения

Redis-клиент автоматически обрабатывает соединения в фоновом режиме:

ts
// Соединение не устанавливается до выполнения команды
const client = new RedisClient();

// Первая команда инициирует соединение
await client.set("key", "value");

// Соединение остаётся открытым для последующих команд
await client.get("key");

// Явно закрыть соединение, когда закончили
client.close();

Вы также можете вручную управлять жизненным циклом соединения:

ts
const client = new RedisClient();

// Явное подключение
await client.connect();

// Выполнение команд
await client.set("key", "value");

// Отключение, когда закончили
client.close();

Базовые операции

Строковые операции

ts
// Установить ключ
await redis.set("user:1:name", "Alice");

// Получить ключ
const name = await redis.get("user:1:name");

// Получить ключ как Uint8Array
const buffer = await redis.getBuffer("user:1:name");

// Удалить ключ
await redis.del("user:1:name");

// Проверить существование ключа
const exists = await redis.exists("user:1:name");

// Установить истечение срока (в секундах)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // истекает через 1 час

// Получить время жизни (в секундах)
const ttl = await redis.ttl("session:123");

Числовые операции

ts
// Установить начальное значение
await redis.set("counter", "0");

// Инкрементировать на 1
await redis.incr("counter");

// Декрементировать на 1
await redis.decr("counter");

Хеш-операции

ts
// Установить несколько полей в хеше
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);

// Получить несколько полей из хеша
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// Получить одно поле из хеша (возвращает значение напрямую, null если отсутствует)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"

// Инкрементировать числовое поле в хеше
await redis.hincrby("user:123", "visits", 1);

// Инкрементировать float-поле в хеше
await redis.hincrbyfloat("user:123", "score", 1.5);

Операции с множествами

ts
// Добавить элемент в множество
await redis.sadd("tags", "javascript");

// Удалить элемент из множества
await redis.srem("tags", "javascript");

// Проверить существование элемента в множестве
const isMember = await redis.sismember("tags", "javascript");

// Получить все элементы множества
const allTags = await redis.smembers("tags");

// Получить случайный элемент
const randomTag = await redis.srandmember("tags");

// Извлечь (удалить и вернуть) случайный элемент
const poppedTag = await redis.spop("tags");

Pub/Sub

Bun предоставляет нативные привязки для протокола Redis Pub/Sub. Новое в Bun 1.2.23

Базовое использование

Для начала публикации сообщений вы можете настроить издателя в publisher.ts:

typescript
import { RedisClient } from "bun";

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

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

writer.close();

В другом файле создайте подписчика в 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(`Получено: ${message}`);
});

В одной оболочке запустите подписчика:

bash
bun run subscriber.ts

и в другой запустите издателя:

bash
bun run publisher.ts

NOTE

Режим подписки занимает соединение `RedisClient`. Клиент с подписками может вызывать только `RedisClient.prototype.subscribe()`. Другими словами, приложениям, которым нужно отправлять сообщения в Redis, нужно отдельное соединение, получаемое через `.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");

Публикация

Публикация сообщений выполняется через метод publish():

typescript
await client.publish(channelName, message);

Подписки

Bun RedisClient позволяет подписываться на каналы через метод .subscribe():

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

Вы можете отписаться через метод .unsubscribe():

typescript
await client.unsubscribe(); // Отписаться от всех каналов.
await client.unsubscribe(channel); // Отписаться от конкретного канала.
await client.unsubscribe(channel, listener); // Отписать конкретного слушателя.

Расширенное использование

Выполнение команд и конвейеризация

Клиент автоматически конвейеризирует команды, улучшая производительность путём отправки нескольких команд пакетом и обработки ответов по мере их поступления.

ts
// Команды автоматически конвейеризируются по умолчанию
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);

Чтобы отключить автоматическую конвейеризацию, вы можете установить опцию enableAutoPipelining в false:

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

Необработанные команды

Когда вам нужно использовать команды, у которых нет удобных методов, вы можете использовать метод send:

ts
// Выполнить любую Redis-команду
const info = await redis.send("INFO", []);

// LPUSH в список
await redis.send("LPUSH", ["mylist", "value1", "value2"]);

// Получить диапазон списка
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

Метод send позволяет использовать любую Redis-команду, даже те, у которых нет выделенных методов в клиенте. Первый аргумент — имя команды, второй аргумент — массив строковых аргументов.

События соединения

Вы можете зарегистрировать обработчики для событий соединения:

ts
const client = new RedisClient();

// Вызывается при успешном подключении к Redis-серверу
client.onconnect = () => {
  console.log("Подключено к Redis-серверу");
};

// Вызывается при отключении от Redis-сервера
client.onclose = error => {
  console.error("Отключено от Redis-сервера:", error);
};

// Ручное подключение/отключение
await client.connect();
client.close();

Статус соединения и мониторинг

ts
// Проверить подключено ли
console.log(client.connected); // boolean указывающий статус подключения

// Проверить количество буферизованных данных (в байтах)
console.log(client.bufferedAmount);

Преобразование типов

Redis-клиент обрабатывает автоматическое преобразование типов для Redis-ответов:

  • Целочисленные ответы возвращаются как JavaScript numbers
  • Bulk strings возвращаются как JavaScript strings
  • Simple strings возвращаются как JavaScript strings
  • Null bulk strings возвращаются как null
  • Array responses возвращаются как JavaScript arrays
  • Error responses выбрасывают JavaScript errors с соответствующими кодами ошибок
  • Boolean responses (RESP3) возвращаются как JavaScript booleans
  • Map responses (RESP3) возвращаются как JavaScript objects
  • Set responses (RESP3) возвращаются как JavaScript arrays

Специальная обработка для конкретных команд:

  • EXISTS возвращает boolean вместо number (1 становится true, 0 становится false)
  • SISMEMBER возвращает boolean (1 становится true, 0 становится false)

Следующие команды отключают автоматическую конвейеризацию:

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

Опции соединения

При создании клиента вы можете передать различные опции для настройки соединения:

ts
const client = new RedisClient("redis://localhost:6379", {
  // Таймаут подключения в миллисекундах (по умолчанию: 10000)
  connectionTimeout: 5000,

  // Таймаут бездействия в миллисекундах (по умолчанию: 0 = нет таймаута)
  idleTimeout: 30000,

  // Автоматически ли переподключаться при отключении (по умолчанию: true)
  autoReconnect: true,

  // Максимальное количество попыток переподключения (по умолчанию: 10)
  maxRetries: 10,

  // Очередировать ли команды при отключении (по умолчанию: true)
  enableOfflineQueue: true,

  // Автоматически ли конвейеризировать команды (по умолчанию: true)
  enableAutoPipelining: true,

  // TLS опции (по умолчанию: false)
  tls: true,
  // Альтернативно, предоставить пользовательскую TLS конфигурацию:
  // tls: {
  //   rejectUnauthorized: true,
  //   ca: "path/to/ca.pem",
  //   cert: "path/to/cert.pem",
  //   key: "path/to/key.pem",
  // }
});

Поведение переподключения

Когда соединение теряется, клиент автоматически пытается переподключиться с экспоненциальной задержкой:

  1. Клиент начинает с небольшой задержки (50 мс) и удваивает её с каждой попыткой
  2. Задержка переподключения ограничена 2000 мс (2 секунды)
  3. Клиент пытается переподключиться до maxRetries раз (по умолчанию: 10)
  4. Команды, выполненные во время отключения:
    • Очередятся, если enableOfflineQueue равен true (по умолчанию)
    • Отклоняются немедленно, если enableOfflineQueue равен false

Поддерживаемые форматы URL

Redis-клиент поддерживает различные форматы URL:

ts
// Стандартный Redis URL
new RedisClient("redis://localhost:6379");
new RedisClient("redis://localhost:6379");

// С аутентификацией
new RedisClient("redis://username:password@localhost:6379");

// С номером базы данных
new RedisClient("redis://localhost:6379/0");

// TLS соединения
new RedisClient("rediss://localhost:6379");
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");

// Unix socket соединения
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");

// TLS через Unix socket
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");

Обработка ошибок

Redis-клиент выбрасывает типизированные ошибки для различных сценариев:

ts
try {
  await redis.get("non-existent-key");
} catch (error) {
  if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
    console.error("Соединение с Redis-сервером было закрыто");
  } else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
    console.error("Аутентификация не удалась");
  } else {
    console.error("Неожиданная ошибка:", error);
  }
}

Распространённые коды ошибок:

  • ERR_REDIS_CONNECTION_CLOSED — Соединение с сервером было закрыто
  • ERR_REDIS_AUTHENTICATION_FAILED — Не удалось аутентифицироваться на сервере
  • ERR_REDIS_INVALID_RESPONSE — Получен неверный ответ от сервера

Примеры использования

Кэширование

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

  // Сначала попробовать получить из кэша
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }

  // Нет в кэше, получить из базы данных
  const user = await database.getUser(userId);

  // Сохранить в кэше на 1 час
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);

  return user;
}

Ограничение частоты запросов

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

  // Инкрементировать счётчик
  const count = await redis.incr(key);

  // Установить срок действия, если это первый запрос в окне
  if (count === 1) {
    await redis.expire(key, windowSecs);
  }

  // Проверить, превышен ли лимит
  return {
    limited: count > limit,
    remaining: Math.max(0, limit - count),
  };
}

Хранение сессий

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

  // Сохранить сессию с истечением срока
  await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
  await redis.expire(key, 86400); // 24 часа

  return sessionId;
}

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

  // Получить данные сессии
  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),
  };
}

Примечания по реализации

Redis-клиент Bun реализован на Zig и использует Redis Serialization Protocol (RESP3). Он эффективно управляет соединениями и обеспечивает автоматическое переподключение с экспоненциальной задержкой.

Клиент поддерживает конвейеризацию команд, что означает, что несколько команд могут быть отправлены без ожидания ответов на предыдущие команды. Это значительно улучшает производительность при последовательной отправке нескольких команд.

Ограничения и будущие планы

Текущие ограничения Redis-клиента, которые мы планируем исправить в будущих версиях:

  • Транзакции (MULTI/EXEC) должны выполняться через необработанные команды на данный момент

Неподдерживаемые функции:

  • Redis Sentinel
  • Redis Cluster

Bun от www.bunjs.com.cn