12k
All articles

Использование встроенного Redis-клиента Bun

Руководство по встроенному Redis-клиенту Bun: подключение, типизированные команды, Pub/Sub, авто-пайплайнинг, rate limiting и когда нужен ioredis.

OpenReplay Team
OpenReplay Team
Использование встроенного Redis-клиента Bun

Bun поставляется со встроенным Redis-клиентом, который импортируется напрямую — import { redis, RedisClient } from "bun" — поэтому для большинства задач кэширования, управления сессиями, pub/sub и ограничения частоты запросов вам больше не нужно добавлять ioredis или node-redis. Встроенный Redis-клиент Bun предоставляет Promise-based API со встроенным управлением подключениями, полностью типизированными ответами и поддержкой TLS, а также поддерживает Redis-сервер версии 7.2 и выше. Это руководство посвящено именно встроенному клиенту, а не ioredis, запущенному на Bun, — именно его молчаливо демонстрирует большинство туториалов по теме «Redis с Bun».

Это различие принципиально. Распространённый паттерн в существующих туториалах по Bun и Redis — выполнить bun add ioredis и вызвать new Redis() — это сторонняя библиотека, а не API среды выполнения. Всё, что описано ниже, использует API, поставляемый непосредственно в составе Bun.

Ключевые выводы

  • Импортируйте клиент через import { redis, RedisClient } from "bun"; стандартный экземпляр redis читает REDIS_URL, затем VALKEY_URL, и при отсутствии обоих использует redis://localhost:6379.
  • Клиент не открывает соединение до выполнения первой команды, автоматически конвейеризирует команды и переподключается с экспоненциальной задержкой: от 50 мс до максимума в 2000 мс при 10 попытках по умолчанию.
  • По состоянию на Bun 1.3 клиент оборачивает 66 Redis-команд в типизированные методы; redis.send(command, argsArray) выполняет любую команду без обёртки — именно так сегодня реализуются MULTI/EXEC.
  • Pub/Sub захватывает соединение целиком, поэтому для публикации или выполнения других команд вызывайте .duplicate(), чтобы получить второе соединение.
  • По состоянию на Bun 1.3.x встроенный клиент не поддерживает Redis Cluster и Sentinel; для этих сценариев используйте node-redis или ioredis.

Подключение с помощью встроенного Redis-клиента

Самый быстрый путь — использовать стандартный экспорт redis, который устанавливает соединение лениво на основе переменных окружения. По умолчанию клиент читает параметры подключения из REDIS_URL, затем из VALKEY_URL, а если ни одна из них не задана, использует redis://localhost:6379. Сокет не открывается до выполнения первой команды.

import { redis, RedisClient } from "bun";

// Стандартный клиент — читает REDIS_URL / VALKEY_URL из окружения
await redis.set("hello", "world");
const result = await redis.get("hello");

// Пользовательский клиент с явным URL
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

Используйте стандартный экземпляр redis для общего доступа в рамках приложения; создавайте собственный RedisClient, когда нужно отдельное соединение, особые параметры или другая база данных. В документации Bun по Redis приведён полный перечень поддерживаемых схем URL, включая rediss:// и redis+tls:// для TLS и redis+unix:// для Unix-сокетов.

Управление подключением берёт на себя клиент:

  • Соединение не устанавливается до выполнения команды; первая команда инициирует подключение, которое остаётся открытым для последующих команд вплоть до вызова client.close().
  • При обрыве соединения клиент восстанавливается самостоятельно: начинает с задержки 50 мс, удваивает её при каждой попытке, ограничивает максимальную задержку значением 2000 мс и повторяет попытки до maxRetries раз (по умолчанию 10).
  • Команды, поступившие в период отключения, ставятся в очередь при enableOfflineQueue: true (по умолчанию) и немедленно отклоняются при значении false.

Это те же механизмы, которые в ioredis пришлось бы настраивать вручную.

Основные операции и резервный метод send()

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

  • целочисленные ответы возвращаются как числа JavaScript;
  • объёмные и простые строки — как строки;
  • null-строки — как null;
  • массивы — как массивы JavaScript.

Предусмотрено и специфическое для команд приведение типов: EXISTS возвращает булево значение вместо числа, SISMEMBER — тоже булево.

Любой из этих параметров можно настроить через объект опций:

const client = new RedisClient("redis://localhost:6379", {
  connectionTimeout: 5000,     // по умолчанию 10000 мс
  autoReconnect: true,         // по умолчанию true
  maxRetries: 10,              // по умолчанию 10
  enableOfflineQueue: true,    // по умолчанию true
  enableAutoPipelining: true,  // по умолчанию true
  tls: true,
});
// Строки и срок жизни
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // секунды
const ttl = await redis.ttl("session:123");

// Счётчики
await redis.set("counter", "0");
await redis.incr("counter");
await redis.decr("counter");

// exists() возвращает булево значение
const present = await redis.exists("session:123"); // true

// Хэши
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com"]);
const [name, email] = await redis.hmget("user:123", ["name", "email"]);

Не для каждой команды есть выделенный метод. Все стандартные операции поддерживаются — включая хэши, списки и множества — в совокупности 66 команд по состоянию на Bun 1.3. Для всего, что выходит за этот набор, используйте send(). Метод send выполняет любую Redis-команду, в том числе те, для которых нет выделенного метода; первый аргумент — имя команды, второй — массив строковых аргументов.

// Резервный вызов команды — имя + массив строковых аргументов
await redis.send("LPUSH", ["mylist", "value1", "value2"]);
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

Через send() сегодня выполняются и транзакции — это основной пробел в API клиента, о котором рассказывается далее.

Pub/Sub через .duplicate()

Pub/Sub поддерживается, однако подписанное соединение не может выполнять никаких других действий. Подписка захватывает соединение RedisClient целиком: клиент с активными подписками может вызывать только subscribe(), поэтому для отправки других команд создайте отдельное соединение с помощью .duplicate(). Callback подписки получает сначала сообщение, затем канал.

import { RedisClient } from "bun";

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

// Второе соединение для публикации / других команд
const subscriber = await redis.duplicate();

await subscriber.subscribe("notifications", (message, channel) => {
  console.log(`[${channel}] ${message}`);
});

await redis.publish("notifications", "Hello from Bun!");

Pub/Sub был добавлен в Bun 1.2.23, и документация по-прежнему помечает его как экспериментальный — на практике стабильный, но стоит зафиксировать версию Bun, если вы на него полагаетесь. Для отписки используйте .unsubscribe() (без аргумента — очищает все каналы; с указанием канала или слушателя — отписывает избирательно).

Автоматическое конвейеризирование

Встроенный клиент использует конвейеризацию по умолчанию — никакого явного включения не требуется. Клиент автоматически объединяет команды в пакеты, повышая производительность за счёт отправки нескольких команд одновременно и обработки ответов по мере их поступления. Несколько команд с await, запущенных вместе (например, через Promise.all), отправляются одним пакетом, а не по одному round-trip каждая:

const [a, b, c] = await Promise.all([
  redis.get("user:1:name"),
  redis.get("user:2:name"),
  redis.get("user:3:name"),
]);

Именно автоматическая конвейеризация обеспечивает разрыв в производительности по сравнению с ioredis. Bun характеризует свой Redis-клиент как значительно более быстрый, чем ioredis, причём преимущество растёт с увеличением размера пакета. Опубликованные и воспроизводимые данные Bun из release notes версии 1.2.9 это подтверждают: в бенчмарке GET Bun.redis оказался быстрее на 44,82% при пакетах из 10 команд, на 58,29% — при пакетах из 100 и на 85,39% — при пакетах из 1000, то есть примерно в 1,85 раза на наибольшем пакете, а не с фиксированным коэффициентом. (В блоге о релизе Bun 1.3 отдельно приводится диаграмма бенчмарка, согласно которой встроенный клиент превышает пропускную способность ioredis более чем в 7,9 раза — это собственное опубликованное число Bun, а не вторичная ссылка.)

Ряд команд отключает автоматическую конвейеризацию, поскольку они сохраняют состояние, — в их числе AUTH, INFO, MULTI, EXEC, WATCH, SUBSCRIBE и SELECT. Чтобы отключить конвейеризацию глобально, установите enableAutoPipelining: false.

Прикладные паттерны: кэш, сессии и ограничение частоты запросов

Приведённые ниже паттерны применимы с любым Redis-клиентом; здесь они реализованы полностью на встроенном API. Рассматривайте это как небольшой сервисный модуль.

Cache-aside. Сначала проверяем Redis, при промахе обращаемся к базе данных, затем заполняем кэш с TTL.

import { redis } from "bun";

async function getUser(userId: string) {
  const cacheKey = `user:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.getUser(userId);       // ваш источник данных
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);          // 1 час
  return user;
}

При write-through обновляйте кэш в той же операции, что и запись в базу данных. При stale-while-revalidate немедленно отдавайте закэшированное значение и обновляйте его в фоне, когда TTL близок к истечению.

Хранение сессий с TTL. Храните сессии в виде хэшей и позвольте Redis автоматически их удалять.

async function createSession(userId: number, data: object) {
  const sessionId = crypto.randomUUID();
  const key = `session:${sessionId}`;
  await redis.hmset(key, [
    "userId", String(userId),
    "created", String(Date.now()),
    "data", JSON.stringify(data),
  ]);
  await redis.expire(key, 86400); // 24 часа
  return sessionId;
}

Ограничение частоты запросов на основе скользящего окна с сортированными множествами. Команды для сортированных множеств не обёрнуты в выделенные методы, поэтому здесь уместно использовать send(). Удаляем записи старше окна, считаем оставшиеся и добавляем текущий запрос.

async function rateLimit(id: string, limit = 100, windowMs = 60_000) {
  const key = `ratelimit:${id}`;
  const now = Date.now();

  await redis.send("ZREMRANGEBYSCORE", [key, "0", String(now - windowMs)]);
  const count = Number(await redis.send("ZCARD", [key]));

  if (count >= limit) return { allowed: false, remaining: 0 };

  await redis.send("ZADD", [key, String(now), `${now}-${crypto.randomUUID()}`]);
  await redis.expire(key, Math.ceil(windowMs / 1000));
  return { allowed: true, remaining: limit - count - 1 };
}

Благодаря автоматической конвейеризации последовательность ZREMRANGEBYSCORE/ZCARD/ZADD/EXPIRE эффективно группируется в пакет без ручного создания pipeline-объектов.

Ограничения и когда всё же стоит использовать ioredis или node-redis

Встроенный клиент покрывает описанные выше типичные сценарии, однако по состоянию на Bun 1.3.x имеет задокументированные ограничения. Транзакции (MULTI/EXEC) выполняются только через низкоуровневые команды, Redis Sentinel и Redis Cluster не поддерживаются. Если вам нужна кластеризация, отказоустойчивость на основе Sentinel или модули Redis Stack (поиск, JSON, временные ряды, вероятностные структуры данных), используйте полнофункциональную библиотеку. В собственном issue Bun, отслеживающем этот переход, рекомендуется node-redis как предпочтительный клиент для новых проектов, а ioredis — как проверенная альтернатива. В блоге о релизе 1.3 отмечается, что поддержка кластеров, streams и Lua-скриптинга находится в разработке — поэтому перед тем как делать выводы о текущем состоянии, сверяйтесь с разделом Limitations в документации: эта область продолжает активно развиваться.

ВозможностьВстроенный redis Bunnode-redis / ioredis
Шаг установкиНе требуется — поставляется с Bunbun add node-redis / ioredis
Автоматическая конвейеризацияВключена по умолчаниюНастраиваемая / ручная
Pub/SubДа (экспериментально, через .duplicate())Да
Транзакции (MULTI/EXEC)Только через send()Нативный API
Redis ClusterНе поддерживаетсяПоддерживается
Redis SentinelНе поддерживаетсяПоддерживается
Модули Redis StackИспользуйте библиотекуПоддерживаются в node-redis

Одна техническая деталь, заслуживающая оговорки: документация описывает клиент как нативно скомпилированный (в настоящее время характеризуется как реализованный на Rust после перехода Bun в 2026 году с Zig). Эта подробность не меняет ни строчки кода приложения — поверхность API одинакова вне зависимости от языка реализации.

Для кэширования, сессий, pub/sub, счётчиков и ограничения частоты запросов встроенного клиента вполне достаточно, а отказ от лишней зависимости — реальное преимущество. Как только понадобится Cluster, Sentinel, Redis Stack или сложные транзакционные сценарии — устанавливайте node-redis. Начните с import { redis } from "bun" против локального сервера, убедитесь, что нужный набор команд поддерживается, и зафиксируйте версию Bun, чтобы эволюция API не преподнесла сюрпризов в продакшене.

Часто задаваемые вопросы

Как выполнить Redis-транзакцию с помощью встроенного клиента Bun?

Встроенный клиент Bun не имеет нативного API для MULTI/EXEC, поэтому транзакции выполняются через низкоуровневый метод send(). Вызовите redis.send('MULTI', []), затем нужные команды, затем redis.send('EXEC', []). Поскольку MULTI, EXEC и WATCH отключают автоматическую конвейеризацию, они отправляются по отдельности, а не пакетом. Если нужен более богатый нативный API транзакций, используйте node-redis или ioredis.

Работает ли встроенный Redis-клиент Bun с Redis Cluster или Sentinel?

Нет. По состоянию на Bun 1.3.x встроенный клиент не поддерживает Redis Cluster и Redis Sentinel согласно разделу Limitations в документации. Для шардированной кластеризации, отказоустойчивости на основе Sentinel или модулей Redis Stack — таких как поиск, JSON и временные ряды — используйте полнофункциональную библиотеку. В собственном migration issue Bun рекомендуется node-redis как предпочтительный клиент для новых проектов, а ioredis — как проверенная альтернатива.

В чём разница между встроенным импортом redis из Bun и запуском ioredis на Bun?

Встроенный клиент — это API среды выполнения, доступный через import { redis, RedisClient } from 'bun', не требующий установки и конвейеризирующий команды по умолчанию. Использование ioredis означает bun add ioredis и new Redis() — стороннюю библиотеку с собственным управлением подключениями. Большинство туториалов по 'Redis с Bun' молчаливо используют именно ioredis. Встроенный клиент поставляется без зависимостей; ioredis добавляет нативный Cluster, Sentinel и API транзакций, которых встроенному клиенту не хватает.

Что происходит с командами, отправленными во время отключения Redis-клиента Bun?

Команды, поступившие в период отключения, ставятся в очередь и воспроизводятся после восстановления соединения, поскольку enableOfflineQueue по умолчанию равен true. Клиент переподключается автоматически с использованием экспоненциальной задержки: начиная с 50 мс, удваивая значение при каждой попытке, с максимумом 2000 мс и 10 попытками по умолчанию. Установите enableOfflineQueue в false, чтобы немедленно отклонять команды во время отключения вместо их буферизации.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.