Форум программистов, компьютерный форум, киберфорум
Reangularity
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Redis и Node.js с TypeScript - решения для высоконагруженных систем

Запись от Reangularity размещена 31.05.2025 в 18:25
Показов 3800 Комментарии 0

Нажмите на изображение для увеличения
Название: fb51e381-d53f-47f9-9296-c82e2c295f14.jpg
Просмотров: 192
Размер:	167.9 Кб
ID:	10864
Redis (Remote Dictionary Server) — сверхбыстрое хранилище данных в памяти, способное обрабатывать операции за микросекунды. И что особенно важно для нас — с удивительно простым API. А теперь добавьте к этому Node.js — асинхронное окружение, созданное для обработки множества параллельных соединений, и TypeScript с его мощной системой типов. Получается технологический трезубец, способный пронзить большинство проблем с производительностью.

За десять лет работы архитектором я заметил занятную закономерность: команды, внедряющие Redis в свои проекты, частенько идут по одному из двух путей. Первый — "давайте закешируем абсолютно всё", что ведёт к неуправляемому хаосу с протухшими данными. Второй — недоверие и минимальное использование, когда команда боится трогать кеш после пары неудачных инцидентов с инвалидацией. Золотая середина требует понимания не только того, КАК использовать Redis с Node.js и TypeScript, но и КОГДА, ГДЕ и ПОЧЕМУ. Именно об этом наш разговор.

Один из моих клиентов на прошлой неделе спросил: "А действительно ли нам нужен Redis? Может, просто добавить еще серверов?" Я ответил вопросом: "А зачем покупать флот грузовиков, если можно построить один хороший склад?" Технически, оба решения могут работать, но эффективность и стоимость будут драматически отличаться.

Теоретические основы Redis



Фундаментальное отличие Redis от традиционных СУБД — хранение всех данных в оперативной памяти, а не на диске. Ключи-значения в Redis живут в RAM, что объясняет его фантастическую скорость — большинство операций выполняются за время порядка O(1), что на практике означает доступ за микросекунды даже при гигантских объёмах данных.

Когда я впервые задумался о внедрении Redis на проекте с миллионами активных пользователей, первый вопрос был "а где подвох?". Ведь нельзя просто взять и ускорить всё в сотни раз без последствий. И подвох, конечно, есть — данные в памяти означают потенциальную потерю при сбоях питания или краше системы. Redis решает эту проблему двумя механизмами персистентности:
RDB (Redis Database) — периодические снапшоты данных на диск,
AOF (Append Only File) — журналирование каждой операции записи.

На практике эти механизмы нередко комбинируют, получая баланс между производительностью и надежностью, хотя я не раз наблюдал, как избыточная настройка персистентности сводила на нет все преимущества Redis по скорости.

В отличие от реляционных баз вроде PostgreSQL или MySQL, Redis предлагает всего пять базовых структур данных, но каких! Каждая из них воплощает концепцию "делай одно, но делай это хорошо":
1. Строки (Strings) — самый простой тип, может хранить текст, числа или бинарные данные.
2. Хеши (Hashes) — коллекции пар ключ-значение, напоминающие JSON-объекты.
3. Списки (Lists) — упорядоченные последовательности строк.
4. Множества (Sets) — неупорядоченные коллекции уникальных строк.
5. Сортированные множества (Sorted Sets) — множества с ассоциированными весами для упорядочивания.
Эти структуры могут показаться примитивными, но их атомарные операции делают Redis идеальным для множества сценариев, недоступных традиционным базам — от реализации очередей до сложных алгоритмов ранжирования.

Архитектурное ограничение, о котором часто забывают — это лимит оперативной памяти. Я помню случай, когда мы запустили Redis на продакшене без настройки политик вытеснения (eviction policies), и через неделю получили аварийное сообщение от системы — Redis просто отказался писать новые данные из-за переполнения памяти.
Основные политики вытеснения включают:
noeviction — отказывать в записи новых ключей при заполнении памяти,
allkeys-lru — удалять наименее недавно использованные ключи,
volatile-lru — удалять наименее используемые ключи с установленным временем жизни,
allkeys-random — удалять случайные ключи.

Выбор политики критически важен и напрямую зависит от характера ваших данных. Для сессионых данных часто подходит volatile-lru, а для кеша запросов — allkeys-lru.

Сравнивая Redis с конкурирующими решениями вроде Memcached или Hazelcast в контексте TypeScript-проектов, нельзя не заметить его выигрышное положение. Memcached предлагает лишь простое key-value хранилище без продвинутых структур данных, а Hazelcast, хоть и предоставляет распределенные коллекции и вычисления, часто оказывается избыточно сложным для стандартных веб-приложений. В своем последнем проекте на Node.js с TypeScript я пробовал все три решения, и Redis с его типовыми клиентами дал наименьшее трение при интеграции.

Хотя Redis чаще всего воспринимают как кеш, он может выступать и в роли полноценной NoSQL базы данных. Это создает интересные гибридные сценарии использования. Например, в одном из финтех-проектов мы храним основные данные транзакций в PostgreSQL, но временные состояния платежей — в Redis. Это позволяет бизнес-логике оперировать миллионами одновременных платежей без нагрузки на основную базу. Типичная ошибка при таком подходе — забывать о ограниченности объема ключа и значения (512 МБ), что все равно делает Redis непригодным для хранения "тяжелых" объектов вроде медиафайлов.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Гибридное использование Redis и SQL базы
async function processPayment(paymentId: string, amount: number): Promise<void> {
  // Сначала проверяем состояние в Redis
  const paymentState = await redisClient.get(`payment:${paymentId}:state`);
  
  if (paymentState === 'processing') {
    throw new Error('Payment already being processed');
  }
  
  // Атомарно устанавливаем состояние и TTL
  await redisClient.multi()
    .set(`payment:${paymentId}:state`, 'processing')
    .expire(`payment:${paymentId}:state`, 300) // Истечет через 5 минут
    .exec();
  
  try {
    // Выполняем тяжелую операцию с основной базой
    await sqlDb.query(
      'INSERT INTO transactions (payment_id, amount, status) VALUES ($1, $2, $3)',
      [paymentId, amount, 'completed']
    );
    
    // Обновляем состояние в Redis
    await redisClient.set(`payment:${paymentId}:state`, 'completed');
  } catch (error) {
    // В случае ошибки отмечаем как неудачную
    await redisClient.set(`payment:${paymentId}:state`, 'failed');
    throw error;
  }
}
При масштабировании Redis выходит на первый план вопрос репликации и шардинга. Встроенная репликация типа master-replica позволяет создавать реплики данных для распределения нагрузки на чтение. Мастер обрабатывает все операции записи, а реплики обслуживают чтение — классическая схема. Но настоящая мощь раскрывается с Redis Cluster — системой шардинга, которая автоматически разделяет данные между несколькими экземплярами Redis. Кластер использует алгоритм консистентного хеширования для распределения ключей, обеспечивая предсказуемость размещения данных даже при изменении топологии кластера. На практике это выглядит примерно так: вы запускаете N экземпляров Redis (обычно минимум 3 мастера и 3 реплики), а клиентская библиотека автоматически определяет, на каком узле находится нужный ключ. Для клиентов на Node.js с TypeScript существуют специальные библиотеки, абстрагирующие эту сложность.

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

Репликация Redis поддерживает как синхронную, так и асинхронную модели. В синхронном режиме мастер блокируется до подтверждения записи от реплик, что замедляет операции, но обеспечивает консистентность. Асинхронная модель — компромис в пользу скорости ценой потенциальной потери данных при отказе мастера. В проектах с критичными для бизнеса данными я всегда настаиваю на синхронной репликации, хотя бы для подмножества особо важных ключей.

Персистентность данных в Redis часто становится тем камнем преткновения, о который спотыкаются даже опытные разработчики. RDB и AOF имеют свои неочевидные тонкости, которые я узнал на собственных граблях. Например, конфигурация RDB через директиву save в формате save <секунды> <изменения> выглядит просто:

TypeScript
1
2
3
save 900 1    # сохранять каждые 15 минут, если изменился хотя бы 1 ключ
save 300 10   # сохранять каждые 5 минут, если изменилось 10 ключей
save 60 10000 # сохранять каждую минуту, если изменилось 10000 ключей
Но незаметная ловушка здесь в том, что каждое условие работает независимо! Стоит одному из них сработать — и Redis начинает создавать снапшот. А когда Redis создает снапшот, он форкает дочерний процесс, который может потреблять значительные ресурсы. На одном проекте мы получили внезапные скачки латентности из-за слишком частых снапшотов, и пришлось срочно пересматривать политику сохранения.

Настройка AOF еще запутаннее. Три режима синхронизации — always, everysec и no — предлагают выбор между надежностью и производительностью:

TypeScript
1
2
appendonly yes  # включить AOF
appendfsync everysec  # синхронизировать раз в секунду (компромиссный вариант)
Режим always гарантирует запись каждой операции на диск, но сильно тормозит систему. Режим no перекладывает решение о сбросе на операционную систему, что быстрее, но риск потери данных выше. Золотая середина — everysec, который в большинстве случаев дает приемлемый баланс. Интересный факт: пользовательская операция может генерировать несколько команд Redis, которые будут записаны в AOF файл. Это приводит к постоянному росту AOF, иногда до гигантских размеров. Для борьбы с этим Redis предлагает механизм перезаписи (AOF rewriting), который сжимает файл, сохраняя только команды, необходимые для воссоздания текущего состояния базы.

Безопасность Redis — отдельный зверь, о котором многие разработчики узнают только после инцидентов. В отличие от традиционных БД, Redis изначально проектировался для работы в доверенной среде. До версии 6.0 не было даже нормальной системы управления доступом — только простой пароль для аутентификации.

С Redis 6.0 появилась полноценная система ACL (Access Control List), позволяющая гранулярно настраивать права пользователей:

TypeScript
1
2
3
# В redis.conf или через команду ACL SETUSER
user default on +@all ~* >mysecretpassword
user restricted on +get ~object:* -@all +@read
Здесь default получает полный доступ ко всем командам и ключам, а restricted может только читать ключи, начинающиеся с object:. Эта система открыла новые возможности для использования Redis в многопользовательских средах. Но я часто наблюдаю, как разработчики игнорируют проблему сетевой безопасности Redis. По умолчанию Redis слушает все сетевые интерфейсы, не требует аутентификации и передает данные в открытом виде. Забыли настроить фаервол — и ваш Redis стал добычей хакеров.

Я однажды провел простой эксперимент: запустил открытый Redis в облаке без защиты. Менее чем через 12 часов он был обнаружен и скомпрометирован — злоумышленник использовал Redis для запуска майнера криптовалюты! С тех пор я параноидально отношусь к безопасности и всегда настаиваю на:
1. Привязке Redis только к локальному интерфейсу (127.0.0.1) в продакшене.
2. Использовании TLS для шифрования трафика между клиентами и сервером.
3. Настройке строгих ACL с пользователями, имеющими минимально необходимые права.
4. Регулярном обновлении Redis до последних версий.

При работе с Redis Sentinel или Cluster защита становится еще сложнее — каждый узел должен быть защищен отдельно, а аутентификация должна быть согласована между всеми компонентами. В контексте Node.js и TypeScript эти настройки безопасности отражаются в конфигурации клиента:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createClient } from 'redis';
 
interface RedisSecurityConfig {
  password: string;
  username?: string;
  tls?: {
    ca?: string;
    cert?: string;
    key?: string;
  };
}
 
// Безопасная конфигурация клиента
const client = createClient({
  url: 'rediss://redis.example.com:6379', // обратите внимание на rediss:// (с TLS)
  username: 'appuser',
  password: process.env.REDIS_PASSWORD, // всегда берите из переменных окружения!
  socket: {
    tls: true,
    rejectUnauthorized: true, // проверять сертификаты
    ca: fs.readFileSync('./ca.crt', 'utf8'),
  }
});
Обратите внимание на протокол rediss:// — это не опечатка, а обозначение защищенного соединения (по аналогии с http/https). Еще один аспект безопасности, который часто игнорируют, — это уязвимость к Redis Lua-инъекциям. Redis поддерживает выполнение скриптов на Lua, и если вы динамически генерируете такие скрипты из пользовательского ввода без должной санитизации — это прямой путь к компрометации системы.

Помимо безопасности, важнейшим аспектом при работе с Redis является понимание его модели транзакций, которая сильно отличается от SQL-баз. Redis предлагает оптимистические транзакции через механизм MULTI/EXEC/WATCH. Важно: Redis не выполняет проверку условий внутри транзакции — все команды в блоке MULTI выполнятся независимо от результатов предыдущих команд, если не произойдет фатальная ошибка синтаксиса. Это порождает неинтуитивное поведение, которое часто становится источником ошибок:

TypeScript
1
2
3
4
5
// Потенциально проблемный код
await client.multi()
  .get('counter')
  .incrBy('counter', 10)
  .exec();
Даже если ключ 'counter' не существует или содержит нечисловое значение, инкремент всё равно будет выполнен без ошибки (создав ключ со значением 10).

Не запускается пакет node js - пакетами? npm? сам node? gulp?
Всем доброго времени суток. Есть такая проблема, пытаюсь перебраться на Linux (Ubuntu) Установил...

Выложил приложение Node js на хост, ошибка (node:12900) [DEP0005] DeprecationWarning: Buffer()
Выложил приложение Node js на хост, ошибка (node:12900) DeprecationWarning: Buffer() is deprecated...

Не могу с решениями задач на node js (я понимаю как их решить на js, но как на node js не знаю)
1) Однажды ковбой Джо решил обзавестись револьвером и пришёл в оружейный магазин. У ковбоя s...

Решение проблемы с высокой загрузкой сервера Redis
Всем привет. Пишу сервак, который принимает запросы и записывает данные в Redis. Столкнулся с такой...


Настройка связки Redis-Node.js-TypeScript



Переходя от теории к практике, я каждый раз вспоминаю один из первых проектов с Redis и Node.js, когда мы неделю мучились с правильной настройкой окружения. Простые вещи иногда оказываются самыми коварными!
Первым делом нужно установить зависимости.

Bash
1
2
3
npm install redis @types/redis
# или если вы используете yarn
yarn add redis @types/redis
Современный клиент Redis для Node.js предоставляет асинхронный API с поддержкой промисов, что идеально вписывается в асинхронную природу Node.js. Но без должной типизации это была бы лишь половина удовольствия. Именно TypeScript превращает работу с Redis из опасных магических строк в структурированный и предсказуемый код. Базовая настройка клиента выглядит примерно так:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createClient, RedisClientType } from 'redis';
 
// Создание типизированного соединения
const client: RedisClientType = createClient({
  url: 'redis://localhost:6379',
  password: process.env.REDIS_PASSWORD // Хорошая практика: использовать переменные окружения
});
 
// Подключение к серверу Redis
async function connectRedis(): Promise<void> {
  await client.connect();
  console.log('Успешно подключились к Redis');
}
 
// Обработка ошибок
client.on('error', (err: Error) => {
  console.error('Ошибка подключения к Redis:', err);
});
 
connectRedis().catch(console.error);
Однако в реальных проектах такой простой подход редко бывает достаточным. Особенно в микросервисной архитектуре, где каждый сервис может нуждаться в нескольких соединениях к Redis для разных целей. Я предпочитаю инкапсулировать логику работы с Redis в отдельном сервисе, реализующем паттерн Singleton:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { createClient, RedisClientType } from 'redis';
 
export class RedisService {
  private static instance: RedisService;
  private client: RedisClientType;
  private isConnected: boolean = false;
 
  private constructor() {
    this.client = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
      password: process.env.REDIS_PASSWORD
    });
 
    this.client.on('error', this.handleError.bind(this));
    this.client.on('connect', () => { this.isConnected = true; });
    this.client.on('end', () => { this.isConnected = false; });
  }
 
  public static getInstance(): RedisService {
    if (!RedisService.instance) {
      RedisService.instance = new RedisService();
    }
    return RedisService.instance;
  }
 
  private handleError(error: Error): void {
    console.error('Redis error:', error);
    // Здесь можно добавить логику уведомления мониторинговых систем
  }
 
  public async connect(): Promise<void> {
    if (!this.isConnected) {
      await this.client.connect();
    }
  }
 
  public async get<T>(key: string): Promise<T | null> {
    const value = await this.client.get(key);
    if (!value) return null;
    try {
      return JSON.parse(value) as T;
    } catch {
      return value as unknown as T;
    }
  }
 
  public async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
    const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
    if (ttlSeconds) {
      await this.client.set(key, stringValue, { EX: ttlSeconds });
    } else {
      await this.client.set(key, stringValue);
    }
  }
 
  // Другие методы...
}
Этот подход имеет несколько преимуществ:
  • Централизованное управление соединением с Redis.
  • Автоматическая сериализация/десериализация данных.
  • Типизация возвращаемых значений.
  • Простое добавление новых методов для работы с разными структурами данных.

Одна из типичных ошибок при работе с Redis в Node.js — это создание нового соединения для каждого запроса. В высоконагруженных приложениях это может быстро привести к исчерпанию сокетов и падению производительности. Решение — использовать пул соединений. К сожалению, официальный клиент Redis не поддерживает пулы напрямую, но мы можем реализовать их с помощью библиотеки generic-pool:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { createClient, RedisClientType } from 'redis';
import { createPool, Pool, Factory } from 'generic-pool';
 
export class RedisConnectionPool {
  private pool: Pool<RedisClientType>;
 
  constructor(redisUrl: string, options: { max: number; min: number }) {
    const factory: Factory<RedisClientType> = {
      create: async () => {
        const client = createClient({ url: redisUrl });
        await client.connect();
        return client;
      },
      destroy: async (client) => {
        await client.quit();
      }
    };
 
    this.pool = createPool(factory, options);
  }
 
  async withClient<T>(operation: (client: RedisClientType) => Promise<T>): Promise<T> {
    const client = await this.pool.acquire();
    try {
      return await operation(client);
    } finally {
      await this.pool.release(client);
    }
  }
 
  async shutdown(): Promise<void> {
    await this.pool.drain();
    await this.pool.clear();
  }
}
Использование такого пула сильно отличается от прямой работы с клиентом:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Создаем пул с 10 соединениями
const redisPool = new RedisConnectionPool('redis://localhost:6379', {
  max: 10,
  min: 2
});
 
// Используем соединение из пула
const userData = await redisPool.withClient(client => 
  client.get(`user:${userId}`)
);
 
// При завершении работы приложения
process.on('SIGTERM', async () => {
  await redisPool.shutdown();
  process.exit(0);
});
Настройка высокодоступного Redis с использованием Sentinel тоже заслуживает отдельного внимания. Redis Sentinel — это система, которая обеспечивает мониторинг, оповещение и автоматическое переключение между экземплярами Redis при отказе. В среде с Sentinel конфигурация клиента меняется:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createClient } from 'redis';
 
const client = createClient({
  url: 'redis://mymaster', // Имя набора Sentinel
  sentinels: [
    { host: 'sentinel1', port: 26379 },
    { host: 'sentinel2', port: 26379 },
    { host: 'sentinel3', port: 26379 }
  ],
  sentinelUsername: 'sentinel-user',
  sentinelPassword: process.env.SENTINEL_PASSWORD,
  username: 'redis-user',
  password: process.env.REDIS_PASSWORD
});

Паттерны кеширования в реальных проектах



Теоретическое понимание Redis – это лишь полдела. Настоящие баталии начинаются, когда вы пытаетесь внедрить кеширование в рабочий проект, особенно если он уже страдает от проблем с производительностью. За годы работы я встречал дюжины проектов, где кеширование внедрялось хаотично, превращаясь из решения проблем в их источник. Существует несколько фундаментальных паттернов кеширования, и знание их особенностей критически важно для правильной архитектуры:

Cache-Aside (Lazy Loading)



Самый распространённый паттерн и, к счастью, самый простой. Приложение сначала проверяет наличие данных в кеше. Если данные отсутствуют (cache miss), они извлекаются из основного хранилища, помещаются в кеш и возвращаются клиенту.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function getUserWithCacheAside(userId: string): Promise<User | null> {
  const cacheKey = `user:${userId}`;
  
  // Сначала проверяем кеш
  const cachedUser = await redisClient.get(cacheKey);
  if (cachedUser) {
    console.log('Cache hit');
    return JSON.parse(cachedUser);
  }
  
  console.log('Cache miss');
  // Если в кеше нет, берем из базы данных
  const user = await database.findUserById(userId);
  
  if (user) {
    // Сохраняем в кеш на 1 час
    await redisClient.set(cacheKey, JSON.stringify(user), { EX: 3600 });
  }
  
  return user;
}
Этот паттерн прост в реализации и эффективен для большинства сценариев, но у него есть несколько подводных камней:
1. Проблема "лавины кеша" (cache stampede) – когда множество запросов одновременно обнаруживают отсутствие данных в кеше и все обращаются к базе.
2. Потенциально устаревшие данные, если TTL слишком велик.
3. Асимметричная нагрузка на базу данных при истечении кеша популярных ключей.

Write-Through



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

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
async function updateUserWithWriteThrough(
  userId: string, 
  userData: Partial<User>
): Promise<User> {
  // Сначала обновляем базу данных
  const updatedUser = await database.updateUser(userId, userData);
  
  // Затем обновляем кеш
  const cacheKey = `user:${userId}`;
  await redisClient.set(cacheKey, JSON.stringify(updatedUser), { EX: 3600 });
  
  return updatedUser;
}
На практике часто комбинируют Cache-Aside и Write-Through для достижения баланса между производительностью и актуальностью данных. Я называю этот подход "двойной защитой":

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class UserRepository {
  private redisClient: RedisClientType;
  private database: Database;
  private readonly TTL = 3600; // 1 час
  
  constructor(redisClient: RedisClientType, database: Database) {
    this.redisClient = redisClient;
    this.database = database;
  }
  
  async getUser(userId: string): Promise<User | null> {
    const cacheKey = `user:${userId}`;
    
    // Cache-Aside
    const cachedUser = await this.redisClient.get(cacheKey);
    if (cachedUser) return JSON.parse(cachedUser);
    
    const user = await this.database.findUserById(userId);
    if (user) {
      await this.redisClient.set(cacheKey, JSON.stringify(user), { EX: this.TTL });
    }
    
    return user;
  }
  
  async updateUser(userId: string, userData: Partial<User>): Promise<User> {
    // Write-Through
    const updatedUser = await this.database.updateUser(userId, userData);
    
    const cacheKey = `user:${userId}`;
    await this.redisClient.set(cacheKey, JSON.stringify(updatedUser), { EX: this.TTL });
    
    return updatedUser;
  }
}
Управление временем жизни данных (TTL) – отдельная наука в мире кеширования. Слишком короткий TTL сводит на нет преимущества кеша, слишком длинный – приводит к устаревшим данным. Вопреки распространенному заблуждению, не существует универсального "правильного" TTL. Я вывел для себя несколько эмпирических правил:
1. Для статичных данных (справочники, конфигурации) – часы или дни,
2. Для полустатичных данных (профили пользователей) – минуты или часы,
3. Для динамичных данных (счетчики, статусы) – секунды или минуты,
4. Для критически важных данных – событийная инвалидация вместо TTL.
В одном проекте мы применили адаптивный TTL, который зависел от частоты изменения данных. Чем чаще данные менялись, тем короче становился их TTL, и наоборот:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class AdaptiveCacheTTL {
  private redisClient: RedisClientType;
  private readonly MIN_TTL = 60; // 1 минута
  private readonly MAX_TTL = 86400; // 24 часа
  
  constructor(redisClient: RedisClientType) {
    this.redisClient = redisClient;
  }
  
  async calculateTTL(key: string): Promise<number> {
    // Ключ для хранения времени последнего изменения
    const lastChangedKey = `${key}:last_changed`;
    // Ключ для хранения предыдущего TTL
    const previousTTLKey = `${key}:previous_ttl`;
    
    const now = Math.floor(Date.now() / 1000);
    const lastChanged = parseInt(await this.redisClient.get(lastChangedKey) || `${now}`);
    const previousTTL = parseInt(await this.redisClient.get(previousTTLKey) || `${this.MIN_TTL}`);
    
    // Вычисляем время с последнего изменения
    const timeSinceChange = now - lastChanged;
    
    // Если прошло больше времени, чем предыдущий TTL, увеличиваем TTL
    let newTTL = previousTTL;
    if (timeSinceChange > previousTTL) {
      newTTL = Math.min(previousTTL * 2, this.MAX_TTL);
    } else {
      // Иначе уменьшаем TTL
      newTTL = Math.max(previousTTL / 2, this.MIN_TTL);
    }
    
    // Сохраняем новый TTL для будущего использования
    await this.redisClient.set(previousTTLKey, newTTL.toString());
    
    return newTTL;
  }
  
  async recordChange(key: string): Promise<void> {
    // Запись времени изменения
    const now = Math.floor(Date.now() / 1000);
    await this.redisClient.set(`${key}:last_changed`, now.toString());
  }
}
Этот подход значительно снизил количество кеш-промахов при сохранении актуальности данных. Однако он добавляет накладные расходы в виде дополнительных обращений к Redis для вычисления TTL, поэтому подходит не для всех сценариев.

Сериализация сложных объектов часто становится узким местом при работе с Redis. По умолчанию Redis хранит строки, и мы вынуждены преобразовывать наши объекты в строки и обратно. Самый очевидный способ – использовать JSON.stringify() и JSON.parse(), но этот подход имеет ряд ограничений:
1. Не сохраняет типы данных TypeScript.
2. Не обрабатывает циклические ссылки.
3. Не может сериализовать специальные объекты JavaScript (Map, Set, Date и т.д. в их родном формате).
4. Занимает больше места, чем бинарные форматы.
На практике я часто использую комбинацию из типизированных методов и бинарных форматов сериализации вроде MessagePack или Protocol Buffers:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import * as msgpack from 'msgpack-lite';
 
class TypedRedisClient<T> {
  private redisClient: RedisClientType;
  private prefix: string;
  
  constructor(redisClient: RedisClientType, prefix: string) {
    this.redisClient = redisClient;
    this.prefix = prefix;
  }
  
  private getKey(id: string): string {
    return `${this.prefix}:${id}`;
  }
  
  async get(id: string): Promise<T | null> {
    const data = await this.redisClient.get(this.getKey(id));
    if (!data) return null;
    
    try {
      // Используем MessagePack для эффективной сериализации
      return msgpack.decode(Buffer.from(data, 'base64')) as T;
    } catch (error) {
      console.error('Ошибка десериализации:', error);
      return null;
    }
  }
  
  async set(id: string, value: T, ttl?: number): Promise<void> {
    const serialized = msgpack.encode(value).toString('base64');
    
    if (ttl) {
      await this.redisClient.set(this.getKey(id), serialized, { EX: ttl });
    } else {
      await this.redisClient.set(this.getKey(id), serialized);
    }
  }
}
 
// Использование с конкретным типом
const userCache = new TypedRedisClient<User>(redisClient, 'user');
await userCache.set('123', { id: '123', name: 'John' }, 3600);
const user = await userCache.get('123'); // Типизировано как User | null
Для еще более эффективной сериализации иногда стоит использовать специфичные для домена сериализаторы. Например, если мы часто кешируем географические координаты, их можно хранить не как объекты, а как строки:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface GeoPoint {
  lat: number;
  lng: number;
}
 
// Сериализация
function serializeGeoPoint(point: GeoPoint): string {
  return `${point.lat.toFixed(6)},${point.lng.toFixed(6)}`;
}
 
// Десериализация
function deserializeGeoPoint(value: string): GeoPoint {
  const [lat, lng] = value.split(',').map(Number);
  return { lat, lng };
}
Такой подход не только экономит место, но и позволяет использовать геопространственные команды Redis напрямую, без дополнительных преобразований.

Проблема консистентности данных в распределенных системах становится особенно острой, когда мы начинаем кешировать данные. Теорема CAP неумолимо напоминает нам, что в распределенной системе невозможно одновременно обеспечить консистентность, доступность и устойчивость к разделению сети – приходится жертвовать чем-то одним. В большинстве веб-приложений мы жертвуем строгой консистентностью в пользу высокой доступности, выбирая "в итоге консистентную" модель (eventually consistent). Это значит, что в некоторые моменты разные пользователи могут видеть разные версии данных, но система гарантирует, что со временем все увидят одну и ту же актуальную версию. Пример реализации этой модели:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
async function updateProduct(productId: string, update: ProductUpdate): Promise<void> {
  // Обновляем в основной базе данных
  await database.updateProduct(productId, update);
  
  // Устанавливаем флаг "грязных данных" в Redis
  await redisClient.set(`product:${productId}:dirty`, '1', { EX: 30 });
  
  // Публикуем событие об обновлении для других сервисов
  await redisClient.publish('product:updates', JSON.stringify({
    productId,
    timestamp: Date.now()
  }));
}
 
async function getProduct(productId: string): Promise<Product | null> {
  const cacheKey = `product:${productId}`;
  const dirtyKey = `${cacheKey}:dirty`;
  
  // Проверяем "грязный" флаг
  const isDirty = await redisClient.get(dirtyKey);
  
  if (!isDirty) {
    const cached = await redisClient.get(cacheKey);
    if (cached) return JSON.parse(cached);
  }
  
  // Получаем свежие данные из БД
  const product = await database.getProduct(productId);
  if (product) {
    // Обновляем кеш и снимаем "грязный" флаг
    await redisClient.set(cacheKey, JSON.stringify(product), { EX: 3600 });
    await redisClient.del(dirtyKey);
  }
  
  return product;
}
Этот подход использует "грязные флаги" для индикации устаревших данных и предотвращает использование неактуального кеша. Одновременно событие обновления рассылается всем заинтересованным сервисам, которые могут обновить свои локальные кеши. Для реализации более строгой модели иногда применяют двухфазный коммит (2PC) с помощью Redis Lua-скриптов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Lua-скрипт для атомарного обновления
const updateLuaScript = `
if redis.call('get', KEYS[1]) == ARGV[1] then
  redis.call('set', KEYS[1], ARGV[2])
  return 1
else
  return 0
end
`;
 
async function atomicUpdate(key: string, expectedValue: string, newValue: string): Promise<boolean> {
  const result = await redisClient.eval(
    updateLuaScript,
    {
      keys: [key],
      arguments: [expectedValue, newValue]
    }
  );
  
  return result === 1;
}
Инвалидация кеша в распределенных системах – еще один вызов, с которым приходится сталкиваться. В моем опыте наиболее удачным решением оказался паттерн "Publish-Subscribe" с использованием механизма Redis Pub/Sub:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Сервис, который меняет данные
class ProductService {
  async updateProduct(productId: string, data: ProductUpdate): Promise<void> {
    // Обновляем базу данных
    await this.database.updateProduct(productId, data);
    
    // Отправляем событие об изменении
    await this.redisClient.publish('cache:invalidate', JSON.stringify({
      entity: 'product',
      id: productId,
      timestamp: Date.now()
    }));
  }
}
 
// Сервис кеширования, который подписывается на события
class CacheService {
  constructor(redisClient: RedisClientType) {
    // Подписываемся на канал инвалидации
    const subscriber = redisClient.duplicate();
    subscriber.connect().then(() => {
      subscriber.subscribe('cache:invalidate', this.handleInvalidation.bind(this));
    });
  }
  
  private async handleInvalidation(message: string): Promise<void> {
    try {
      const { entity, id, timestamp } = JSON.parse(message);
      // Удаляем устаревшие данные из кеша
      await this.redisClient.del(`${entity}:${id}`);
      console.log(`Кеш инвалидирован: ${entity}:${id}`);
    } catch (error) {
      console.error('Ошибка при инвалидации кеша:', error);
    }
  }
}

Продвинутые структуры данных Redis



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

Хеши (Hashes) — пожалуй, самая недооцененная структура данных Redis. По сути это таблицы пар ключ-значение внутри одного ключа Redis. Идеальны для моделирования объектов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function storeUserProfile(userId: string, profile: UserProfile): Promise<void> {
  // Преобразуем все поля объекта в строки
  const hash: Record<string, string> = {};
  for (const [key, value] of Object.entries(profile)) {
    hash[key] = typeof value === 'string' ? value : JSON.stringify(value);
  }
  
  // Атомарно сохраняем весь профиль
  await redisClient.hSet(`user:${userId}:profile`, hash);
  
  // Устанавливаем время жизни для всего объекта
  await redisClient.expire(`user:${userId}:profile`, 86400);
}
 
// Обновление только одного поля, без загрузки всего объекта
async function updateUserLastLogin(userId: string): Promise<void> {
  await redisClient.hSet(
    [INLINE]user:${userId}:profile[/INLINE],
    'lastLoginAt',
    new Date().toISOString()
  );
}
Их преимущество над простыми строковыми ключами в том, что можно обновлять отдельные поля, не перезаписывая весь обьект — это особенно ценно для крупных структур. В одном из проектов для финтех-компании мы использовали хеши для кеширования данных по кредитным картам клиентов. Каждая карта представлялась как хеш, что позволяло атомарно обновлять баланс без риска потери других атрибутов.

Множества (Sets) — коллекции уникальных значений. Они незаменимы, когда требуется проверять вхождение элемента или исключать дубликаты:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Отслеживание IP-адресов, посетивших сайт
async function trackVisitor(ip: string): Promise<void> {
  await redisClient.sAdd('visitors:today', ip);
}
 
// Подсчет уникальных посетителей
async function getUniqueVisitorsCount(): Promise<number> {
  return redisClient.sCard('visitors:today');
}
 
// Проверка, был ли посетитель ранее
async function isReturningVisitor(ip: string): Promise<boolean> {
  return redisClient.sIsMember('visitors:yesterday', ip);
}
Я обнаружил, что множества особенно полезны при реализации систем тегирования. Каждый тег — отдельное множество, содержащее идентификаторы обьектов с этим тегом.

Сортированные множества (Sorted Sets) — это настоящие швейцарские ножи Redis. Они сочетают уникальность множеств с возможностью сортировки по числовому весу (score):

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Добавление статьи в рейтинг популярности
async function trackArticleView(articleId: string): Promise<void> {
  // Инкрементируем счетчик просмотров
  await redisClient.zIncrBy('articles:popular', 1, articleId);
  
  // Удаляем старые записи (более 100)
  const totalItems = await redisClient.zCard('articles:popular');
  if (totalItems > 100) {
    await redisClient.zRemRangeByRank('articles:popular', 0, totalItems - 101);
  }
}
 
// Получение топ-10 статей
async function getTopArticles(): Promise<string[]> {
  return redisClient.zRange('articles:popular', -10, -1, { REV: true });
}
Операции с сортированными множествами выполняются за логарифмическое время, что делает их идеальными для рейтингов, лидербордов и временных индексов.

Геопространственные индексы — особый случай сортированных множеств, оптимизированный для работы с географическими координатами:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Сохранение местоположения пользователя
async function updateUserLocation(userId: string, lat: number, lng: number): Promise<void> {
  await redisClient.geoAdd('user:locations', {
    longitude: lng,
    latitude: lat,
    member: userId
  });
}
 
// Поиск ближайших пользователей в радиусе 5 км
async function findNearbyUsers(lat: number, lng: number): Promise<string[]> {
  const results = await redisClient.geoRadius('user:locations', {
    longitude: lng,
    latitude: lat,
    radius: 5,
    unit: 'km'
  });
  
  return results.map(item => item.member);
}
В одном из проектов доставки еды эта функциональность позволила нам реализовать поиск ближайших курьеров без необходимости в специализированых геоинформационных системах.

Redis Streams – относительно новое дополнение, появившееся в Redis 5.0, которое предоставляет аппенд-онли журнал событий с уникальными идентификаторами. Streams решают многие проблемы традиционных систем очередей, обеспечивая надежное хранение сообщений с возможностью параллельной обработки группами потребителей:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Добавление события в поток
async function logUserAction(userId: string, action: string, metadata: Record<string, string>): Promise<string> {
  // Формируем данные события (все значения должны быть строками)
  const eventData = {
    userId,
    action,
    timestamp: Date.now().toString(),
    ...metadata
  };
  
  // Добавляем в поток, получаем уникальный ID
  return redisClient.xAdd('user:actions', '*', eventData);
}
 
// Чтение последних событий
async function getRecentActions(limit: number = 10): Promise<Array<UserAction>> {
  const events = await redisClient.xRevRange('user:actions', '+', '-', { COUNT: limit });
  
  return events.map(event => {
    // Преобразуем формат Redis в удобный TypeScript объект
    const { id, message } = event;
    return {
      id,
      userId: message.userId,
      action: message.action,
      timestamp: parseInt(message.timestamp),
      metadata: Object.fromEntries(
        Object.entries(message).filter(([key]) => 
          !['userId', 'action', 'timestamp'].includes(key)
        )
      )
    };
  });
}
Для построения реальнго микросервиса на потоках особенно полезен паттерн с группами потребителей:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Создаем группу потребителей (выполняется один раз)
async function setupConsumerGroup(): Promise<void> {
  try {
    await redisClient.xGroupCreate('orders:new', 'order-processors', '0', {
      MKSTREAM: true // создать поток, если он не существует
    });
  } catch (err: any) {
    // Игнорируем ошибку, если группа уже существует
    if (!err.message.includes('BUSYGROUP')) throw err;
  }
}
 
// Обработчик заказов в отдельном микросервисе
async function processOrders(consumerId: string): Promise<void> {
  while (true) {
    try {
      // Читаем новые сообщения для этого потребителя
      const messages = await redisClient.xReadGroup({
        group: 'order-processors',
        consumer: consumerId,
        streams: {
          'orders:new': '>' // '>' означает "только новые сообщения"
        },
        count: 10,
        block: 5000 // ждем до 5 секунд
      });
      
      if (!messages || messages.length === 0) continue;
      
      for (const message of messages[0].messages) {
        // Обрабатываем заказ
        await processOrder(message.message);
        
        // Подтверждаем обработку
        await redisClient.xAck('orders:new', 'order-processors', message.id);
      }
    } catch (error) {
      console.error('Ошибка обработки заказов:', error);
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}
HyperLogLog – специализированная структура данных для эффективного подсчета уникальных элементов с минимальным использованием памяти. Она жертвует абсолютной точностью (погрешность около 0.81%) ради экономии ресурсов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Учет просмотра страницы уникальным пользователем
async function trackPageView(pageId: string, userId: string): Promise<void> {
  await redisClient.pfAdd(`pageviews:${pageId}`, userId);
}
 
// Получение приблизительного числа уникальных просмотров
async function getUniqueViewsCount(pageId: string): Promise<number> {
  return redisClient.pfCount(`pageviews:${pageId}`);
}
 
// Объединение статистики по нескольким страницам
async function getCategoryViewsCount(categoryId: string, pageIds: string[]): Promise<number> {
  const keys = pageIds.map(id => `pageviews:${id}`);
  await redisClient.pfMerge(`category:${categoryId}:views`, keys);
  return redisClient.pfCount(`category:${categoryId}:views`);
}

Производительность и мониторинг



Любой архитектор с опытом подтвердит: Redis может превратить черепаху в гепарда, но и гепард спотыкается, если не следить за его здоровьем. Мониторинг производительности Redis — это не просто красивые графики, а жизненно важная составляющая стабильной работы высоконагруженных систем.

Начнём с профилирования. В Node.js с TypeScript важно понимать, где именно возникают задержки: в коде приложения или в Redis. Я регулярно использую инструмент redis-cli с командой MONITOR, чтобы увидеть реальные запросы в реальном времени. Но не забывайте: включение мониторинга значительно снижает производительность, поэтому на продакшене лучше использовать его кратковременно или на реплике.

Bash
1
2
# Запуск мониторинга на 10 секунд
redis-cli MONITOR | head -n 100
Для Node.js приложений лучше встроить измерение времени операций с Redis прямо в код:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class RedisMetrics {
private metrics: Map<string, { count: number; totalTime: number }> = new Map();
 
async measureRedisOperation<T>(
  operationName: string, 
  operation: () => Promise<T>
): Promise<T> {
  const startTime = process.hrtime.bigint();
  
  try {
    return await operation();
  } finally {
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - startTime) / 1_000_000; // в миллисекундах
    
    const stats = this.metrics.get(operationName) || { count: 0, totalTime: 0 };
    stats.count++;
    stats.totalTime += duration;
    this.metrics.set(operationName, stats);
  }
}
 
getMetrics(): Record<string, { count: number; avgTime: number }> {
  const result: Record<string, { count: number; avgTime: number }> = {};
  
  for (const [op, stats] of this.metrics.entries()) {
    result[op] = {
      count: stats.count,
      avgTime: stats.totalTime / stats.count
    };
  }
  
  return result;
}
}
 
// Использование
const redisMetrics = new RedisMetrics();
const users = await redisMetrics.measureRedisOperation(
'get_users_by_role',
() => redisClient.lRange('users:admin', 0, -1)
);
 
// Периодический вывод метрик
setInterval(() => {
console.log(redisMetrics.getMetrics());
}, 60000);
Одна из главных ловушек производительности Redis — это неправильные паттерны доступа. Возьмем классический антипаттерн — выполнение сложных операций в цикле:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Медленный код - N запросов к Redis
async function getProductPrices(productIds: string[]): Promise<Record<string, number>> {
const result: Record<string, number> = {};
for (const id of productIds) {
  const price = await redisClient.get(`product:${id}:price`);
  if (price) result[id] = parseFloat(price);
}
return result;
}
 
// Оптимизированный код - 1 запрос с pipeline
async function getProductPricesFast(productIds: string[]): Promise<Record<string, number>> {
const pipeline = redisClient.multi();
productIds.forEach(id => {
  pipeline.get(`product:${id}:price`);
});
 
const prices = await pipeline.exec();
const result: Record<string, number> = {};
 
prices.forEach((price, index) => {
  if (price) result[productIds[index]] = parseFloat(price as string);
});
 
return result;
}
Для долгосрочного мониторинга Redis предоставляет множество встроенных метрик через команду INFO:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function gatherRedisMetrics(): Promise<Record<string, any>> {
const info = await redisClient.info();
const sections = info.split('#');
  
const metrics: Record<string, any> = {};
  
for (const section of sections) {
  const lines = section.trim().split('\r\n');
  if (lines.length <= 1) continue;
    
  const sectionName = lines[0].replace(/# /, '').toLowerCase();
  metrics[sectionName] = {};
    
  for (let i = 1; i < lines.length; i++) {
    const [key, value] = lines[i].split(':');
    if (key && value !== undefined) {
      // Пробуем преобразовать строковые значения в числа где это возможно
      const numValue = Number(value);
      metrics[sectionName][key] = isNaN(numValue) ? value : numValue;
    }
  }
}
  
return metrics;
}
Ключевые метрики, на которые стоит обратить внимание:
used_memory и used_memory_peak — текущее и пиковое использование памяти,
evicted_keys — количество ключей, удаленных из-за политики вытеснения,
expired_keys — количество ключей, удаленных из-за истечения TTL,
keyspace_hits и keyspace_misses — соотношение попаданий/промахов в кеш,
connected_clients — количество подключенных клиентов,
instantaneous_ops_per_sec — текущая пропускная способность.

Критически важно следить за соотношением промахов и попаданий в кеш (keyspace_hits / (keyspace_hits + keyspace_misses)). Если это соотношение падает ниже 80%, вероятно, ваша стратегия кеширования требует пересмотра. Утечки памяти в Redis обычно связаны с неограниченным ростом коллекций или отсутствием TTL для ключей. Я всегда рекомендую устанавливать жесткие лимиты для структур данных, например, через команду LTRIM для списков:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// Ограничение списка последних уведомлений
async function addNotification(userId: string, notification: Notification): Promise<void> {
const key = `user:${userId}:notifications`;
  
// Добавляем новое уведомление в начало списка
await redisClient.lPush(key, JSON.stringify(notification));
  
// Ограничиваем размер списка 100 элементами
await redisClient.lTrim(key, 0, 99);
  
// Устанавливаем TTL для всего списка, если это новый ключ
await redisClient.expire(key, 604800); // 7 дней
}
Для отслеживания больших ключей, которые могут быть причиной проблем с памятью, Redis предоставляет команду MEMORY USAGE:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function findBiggestKeys(limit: number = 10): Promise<Array<{ key: string; size: number }>> {
  // Получаем список всех ключей
  const keys = await redisClient.keys('*');
  
  const keySizes = await Promise.all(
    keys.map(async key => ({
      key,
      size: await redisClient.memoryUsage(key)
    }))
  );
  
  // Сортируем по размеру и возвращаем топ N
  return keySizes
    .sort((a, b) => b.size - a.size)
    .slice(0, limit);
}
Этот код может пригодиться для выявления "обжор памяти", но имейте в виду, что команда KEYS * блокирует Redis на время выполнения, поэтому лучше запускать такой анализ на реплике или в периоды низкой активности.
Тестирование стратегий кеширования требует особого подхода. Я часто встречаю ситуацию, когда команды разработчиков внедряют кеширование, но не тестируют его эффективность в реальных условиях. Обычные unit-тесты недостаточны — необходимы нагрузочные и интеграционные тесты.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Тестирование стратегии кеширования с симуляцией нагрузки
async function benchmarkCacheStrategy(
  strategy: 'write-through' | 'cache-aside' | 'write-behind',
  concurrency: number,
  operations: number
): Promise<{ hits: number; misses: number; avgResponseTime: number }> {
  const start = Date.now();
  let hits = 0, misses = 0;
  
  // Симуляция параллельных запросов
  const tasks = Array.from({ length: operations }).map(async (_, i) => {
    const userId = (i % (concurrency / 2)) + 1; // Повторяющиеся ID для тестирования hit rate
    
    switch (strategy) {
      case 'write-through':
        return userService.getUserWithWriteThrough(userId.toString());
      case 'cache-aside':
        return userService.getUserWithCacheAside(userId.toString());
      case 'write-behind':
        return userService.getUserWithWriteBehind(userId.toString());
    }
  });
  
  // Используем batching для контроля конкурентности
  const batchSize = Math.min(concurrency, 100);
  const results = [];
  
  for (let i = 0; i < operations; i += batchSize) {
    const batch = tasks.slice(i, i + batchSize);
    results.push(...await Promise.all(batch));
  }
  
  // Получаем статистику из сервиса
  const stats = await redisClient.info('stats');
  const matches = stats.match(/keyspace_hits:(\d+)/);
  hits = matches ? parseInt(matches[1]) : 0;
  
  const missMatches = stats.match(/keyspace_misses:(\d+)/);
  misses = missMatches ? parseInt(missMatches[1]) : 0;
  
  return {
    hits,
    misses,
    avgResponseTime: (Date.now() - start) / operations
  };
}
Distributed tracing становится необходимостью для отладки микросервисных приложений, использующих Redis. Интеграция с OpenTelemetry позволяет увидеть, как запросы проходят через все сервисы, включая Redis:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { trace } from '@opentelemetry/api';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
 
// Регистрация инструментации Redis
registerInstrumentations({
  instrumentations: [
    new RedisInstrumentation({
      // Трассировка только команд, которые выполняются дольше 5мс
      dbStatementSerializer: (cmd, args) => 
        args && args.length > 0 ? `${cmd} ${args[0]}` : cmd
    })
  ]
});
 
// Обертка для трассировки Redis-операций
async function tracedRedisOperation<T>(
  name: string, 
  operation: () => Promise<T>
): Promise<T> {
  const tracer = trace.getTracer('redis-operations');
  
  return tracer.startActiveSpan(name, async (span) => {
    try {
      const result = await operation();
      span.end();
      return result;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      span.end();
      throw error;
    }
  });
}
Эта трасировка незаменима, когда приложение тормозит и нужно понять, какой именно сервис или Redis-команда стала узким местом.

Пример - система рекомендаций с кешированием



Пришло время воплотить все наши теоретические знания в конкретную систему. Давайте построим сервис рекомендаций с кешированием — задачу, с которой я недавно столкнулся при разработке интернет-магазина. Такая система должна оперативно предлагать пользователям товары на основе их предпочтений, при этом выдерживая высокую нагрузку в часы пик.
Начнем с определения основных типов и интерфейсов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/types/index.ts
export interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  tags: string[];
}
 
export interface User {
  id: string;
  purchaseHistory: string[]; // идентификаторы купленных товаров
  viewHistory: string[];     // идентификаторы просмотренных товаров
  preferences: {
    categories: Record<string, number>; // категория -> вес предпочтения
    tags: Record<string, number>;       // тег -> вес предпочтения
  };
}
 
export interface RecommendationScore {
  productId: string;
  score: number;
}
 
export interface RecommendationEngine {
  getRecommendations(userId: string, limit?: number): Promise<Product[]>;
  updateUserPreferences(userId: string, product: Product, action: 'view' | 'purchase'): Promise<void>;
}
Теперь реализуем модуль конфигурации Redis:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/config/redis.ts
import { createClient, RedisClientType } from 'redis';
 
export class RedisManager {
  private static instance: RedisManager;
  private client: RedisClientType;
  
  private constructor() {
    this.client = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
      password: process.env.REDIS_PASSWORD,
    });
    
    this.client.on('error', (err) => {
      console.error('Redis error:', err);
    });
  }
  
  public static getInstance(): RedisManager {
    if (!this.instance) {
      this.instance = new RedisManager();
    }
    return this.instance;
  }
  
  public async connect(): Promise<void> {
    await this.client.connect();
  }
  
  public getClient(): RedisClientType {
    return this.client;
  }
  
  public async disconnect(): Promise<void> {
    await this.client.quit();
  }
}
Ключевая часть системы — репозиторий для работы с пользователями и их предпочтениями:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// src/repositories/UserRepository.ts
import { RedisClientType } from 'redis';
import { User, Product } from '../types';
 
export class UserRepository {
  private client: RedisClientType;
  private readonly TTL = 86400; // 24 часа
  
  constructor(redisClient: RedisClientType) {
    this.client = redisClient;
  }
  
  async getUser(userId: string): Promise<User | null> {
    const key = `user:${userId}`;
    const cachedUser = await this.client.get(key);
    
    if (cachedUser) {
      return JSON.parse(cachedUser);
    }
    
    // В реальном проекте здесь был бы запрос к основной БД
    const user = await this.fetchUserFromDatabase(userId);
    
    if (user) {
      await this.client.set(key, JSON.stringify(user), { EX: this.TTL });
    }
    
    return user;
  }
  
  async updateUserPreferences(
    userId: string, 
    product: Product, 
    action: 'view' | 'purchase'
  ): Promise<void> {
    const user = await this.getUser(userId);
    if (!user) return;
    
    // Обновляем историю просмотров/покупок
    if (action === 'view') {
      if (!user.viewHistory.includes(product.id)) {
        user.viewHistory.push(product.id);
      }
    } else {
      if (!user.purchaseHistory.includes(product.id)) {
        user.purchaseHistory.push(product.id);
      }
    }
    
    // Обновляем предпочтения по категориям
    const categoryWeight = action === 'purchase' ? 2 : 1;
    user.preferences.categories[product.category] = 
      (user.preferences.categories[product.category] || 0) + categoryWeight;
    
    // Обновляем предпочтения по тегам
    const tagWeight = action === 'purchase' ? 1.5 : 0.5;
    for (const tag of product.tags) {
      user.preferences.tags[tag] = 
        (user.preferences.tags[tag] || 0) + tagWeight;
    }
    
    // Сохраняем обновленного пользователя в Redis (Write-Through)
    const key = `user:${userId}`;
    await this.client.set(key, JSON.stringify(user), { EX: this.TTL });
    
    // Инвалидируем кеш рекомендаций для этого пользователя
    await this.client.del(`recommendations:${userId}`);
    
    // В реальном проекте здесь также был бы апдейт основной БД
    await this.updateUserInDatabase(user);
  }
  
  // Имитация работы с базой данных
  private async fetchUserFromDatabase(userId: string): Promise<User | null> {
    // В реальном проекте здесь был бы запрос к основной БД
    return {
      id: userId,
      purchaseHistory: [],
      viewHistory: [],
      preferences: {
        categories: {},
        tags: {}
      }
    };
  }
  
  private async updateUserInDatabase(user: User): Promise<void> {
    // В реальном проекте здесь был бы апдейт основной БД
    console.log(`Обновлен пользователь ${user.id} в базе данных`);
  }
}
Теперь перейдем к хранилищу продуктов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// src/repositories/ProductRepository.ts
import { RedisClientType } from 'redis';
import { Product } from '../types';
 
export class ProductRepository {
  private client: RedisClientType;
  private readonly TTL = 3600; // 1 час
  
  constructor(redisClient: RedisClientType) {
    this.client = redisClient;
  }
  
  async getProduct(productId: string): Promise<Product | null> {
    const key = `product:${productId}`;
    const cachedProduct = await this.client.get(key);
    
    if (cachedProduct) {
      return JSON.parse(cachedProduct);
    }
    
    // В реальном проекте здесь был бы запрос к основной БД
    const product = await this.fetchProductFromDatabase(productId);
    
    if (product) {
      await this.client.set(key, JSON.stringify(product), { EX: this.TTL });
    }
    
    return product;
  }
  
  async getProductsByIds(productIds: string[]): Promise<Product[]> {
    if (productIds.length === 0) return [];
    
    // Используем pipeline для эффективного получения множества продуктов
    const pipeline = this.client.multi();
    productIds.forEach(id => {
      pipeline.get(`product:${id}`);
    });
    
    const results = await pipeline.exec();
    const products: Product[] = [];
    
    // Собираем найденные в кеше продукты
    const missingIds: string[] = [];
    results.forEach((result, index) => {
      if (result) {
        try {
          products.push(JSON.parse(result as string));
        } catch {
          missingIds.push(productIds[index]);
        }
      } else {
        missingIds.push(productIds[index]);
      }
    });
    
    // Подгружаем отсутствующие продукты из БД
    if (missingIds.length > 0) {
      const missingProducts = await this.fetchProductsFromDatabase(missingIds);
      
      // Кешируем подгруженные продукты
      const cachePipeline = this.client.multi();
      missingProducts.forEach(product => {
        cachePipeline.set(
          [INLINE]product:${product.id}[/INLINE], 
          JSON.stringify(product), 
          { EX: this.TTL }
        );
        products.push(product);
      });
      
      await cachePipeline.exec();
    }
    
    return products;
  }
  
  // Имитация работы с базой данных
  private async fetchProductFromDatabase(productId: string): Promise<Product | null> {
    // В реальном проекте здесь был бы запрос к основной БД
    return {
      id: productId,
      name: [INLINE]Товар ${productId}[/INLINE],
      category: 'default',
      price: 100,
      tags: ['new']
    };
  }
  
  private async fetchProductsFromDatabase(productIds: string[]): Promise<Product[]> {
    return Promise.all(
      productIds.map(id => this.fetchProductFromDatabase(id))
    ).then(results => results.filter(Boolean) as Product[]);
  }
}
Теперь реализуем основной движок рекомендаций, где Redis проявит свою силу. Здесь я применил сортированные множества для хранения скоринга рекомендаций — идеальная структура для этой задачи:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// src/services/RecommendationEngine.ts
import { RedisClientType } from 'redis';
import { Product, User, RecommendationScore } from '../types';
import { UserRepository } from '../repositories/UserRepository';
import { ProductRepository } from '../repositories/ProductRepository';
 
export class RedisRecommendationEngine {
private client: RedisClientType;
private userRepo: UserRepository;
private productRepo: ProductRepository;
private readonly RECOMMENDATION_TTL = 1800; // 30 минут
 
constructor(
  redisClient: RedisClientType,
  userRepo: UserRepository,
  productRepo: ProductRepository
) {
  this.client = redisClient;
  this.userRepo = userRepo;
  this.productRepo = productRepo;
}
 
async getRecommendations(userId: string, limit: number = 10): Promise<Product[]> {
  // Проверяем кеш рекомендаций
  const cacheKey = `recommendations:${userId}`;
  const cachedIds = await this.client.zRange(cacheKey, 0, limit - 1, { REV: true });
  
  if (cachedIds.length > 0) {
    // Если есть кешированные рекомендации, возвращаем их
    return this.productRepo.getProductsByIds(cachedIds);
  }
  
  // Если кеша нет, вычисляем рекомендации
  const user = await this.userRepo.getUser(userId);
  if (!user) return [];
  
  // Получаем скоры для рекомендаций
  const scores = await this.calculateRecommendationScores(user);
  
  // Сохраняем скоры в сортированное множество
  if (scores.length > 0) {
    const pipeline = this.client.multi();
    
    // Добавляем все скоры в Redis
    scores.forEach(score => {
      pipeline.zAdd(cacheKey, { score: score.score, value: score.productId });
    });
    
    // Устанавливаем TTL для кеша рекомендаций
    pipeline.expire(cacheKey, this.RECOMMENDATION_TTL);
    
    await pipeline.exec();
    
    // Получаем топ N рекомендаций
    const topProductIds = scores
      .sort((a, b) => b.score - a.score)
      .slice(0, limit)
      .map(s => s.productId);
    
    return this.productRepo.getProductsByIds(topProductIds);
  }
  
  return [];
}
 
private async calculateRecommendationScores(user: User): Promise<RecommendationScore[]> {
  // В реальной системе здесь будет сложная логика вычисления рекомендаций
  // Например, коллаборативная фильтрация или контентно-ориентированные рекомендации
  
  // Для примера реализуем простую систему на основе категорий и тегов
  
  // 1. Получаем все популярные продукты
  const popularProducts = await this.getPopularProducts(100);
  
  // 2. Исключаем продукты, которые пользователь уже купил
  const candidateProducts = popularProducts.filter(
    product => !user.purchaseHistory.includes(product.id)
  );
  
  // 3. Рассчитываем скор для каждого продукта
  return candidateProducts.map(product => {
    let score = 0;
    
    // Базовый скор популярности
    score += 1;
    
    // Добавляем скор на основе предпочтений категорий
    if (user.preferences.categories[product.category]) {
      score += user.preferences.categories[product.category] * 2;
    }
    
    // Добавляем скор на основе предпочтений тегов
    for (const tag of product.tags) {
      if (user.preferences.tags[tag]) {
        score += user.preferences.tags[tag];
      }
    }
    
    // Бонус за просмотр товара, но не покупку (возможно, пользователь заинтересован)
    if (user.viewHistory.includes(product.id) && !user.purchaseHistory.includes(product.id)) {
      score += 3;
    }
    
    return { productId: product.id, score };
  });
}
 
private async getPopularProducts(limit: number): Promise<Product[]> {
  // В реальной системе здесь был бы запрос к аналитической БД
  // Для примера вернем случайные продукты
  const productIds = Array.from({ length: limit }, (_, i) => (i + 1).toString());
  return this.productRepo.getProductsByIds(productIds);
}
 
async updateUserPreferences(
  userId: string, 
  product: Product, 
  action: 'view' | 'purchase'
): Promise<void> {
  // Делегируем обновление предпочтений пользователя в репозиторий
  await this.userRepo.updateUserPreferences(userId, product, action);
  
  // Инвалидируем кеш рекомендаций
  await this.client.del(`recommendations:${userId}`);
}
}
Наконец, создадим HTTP-контроллер для доступа к системе рекомендаций:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// src/controllers/RecommendationController.ts
import { Request, Response } from 'express';
import { RedisRecommendationEngine } from '../services/RecommendationEngine';
import { ProductRepository } from '../repositories/ProductRepository';
 
export class RecommendationController {
private engine: RedisRecommendationEngine;
private productRepo: ProductRepository;
 
constructor(engine: RedisRecommendationEngine, productRepo: ProductRepository) {
  this.engine = engine;
  this.productRepo = productRepo;
}
 
async getRecommendations(req: Request, res: Response): Promise<void> {
  try {
    const userId = req.params.userId;
    const limit = parseInt(req.query.limit as string) || 10;
    
    const recommendations = await this.engine.getRecommendations(userId, limit);
    
    res.json({ success: true, data: recommendations });
  } catch (error) {
    console.error('Ошибка получения рекомендаций:', error);
    res.status(500).json({ 
      success: false, 
      error: 'Внутренняя ошибка сервера' 
    });
  }
}
 
async trackProductView(req: Request, res: Response): Promise<void> {
  try {
    const { userId, productId } = req.params;
    
    const product = await this.productRepo.getProduct(productId);
    if (!product) {
      res.status(404).json({ success: false, error: 'Товар не найден' });
      return;
    }
    
    await this.engine.updateUserPreferences(userId, product, 'view');
    
    res.json({ success: true });
  } catch (error) {
    console.error('Ошибка при отслеживании просмотра товара:', error);
    res.status(500).json({ 
      success: false, 
      error: 'Внутренняя ошибка сервера' 
    });
  }
}
 
async trackProductPurchase(req: Request, res: Response): Promise<void> {
  try {
    const { userId, productId } = req.params;
    
    const product = await this.productRepo.getProduct(productId);
    if (!product) {
      res.status(404).json({ success: false, error: 'Товар не найден' });
      return;
    }
    
    await this.engine.updateUserPreferences(userId, product, 'purchase');
    
    res.json({ success: true });
  } catch (error) {
    console.error('Ошибка при отслеживании покупки товара:', error);
    res.status(500).json({ 
      success: false, 
      error: 'Внутренняя ошибка сервера' 
    });
  }
}
}
Для полноты картины, покажем инициализацию и запуск сервиса:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// src/index.ts
import express from 'express';
import { RedisManager } from './config/redis';
import { UserRepository } from './repositories/UserRepository';
import { ProductRepository } from './repositories/ProductRepository';
import { RedisRecommendationEngine } from './services/RecommendationEngine';
import { RecommendationController } from './controllers/RecommendationController';
 
async function bootstrap() {
  // Инициализация Redis
  const redisManager = RedisManager.getInstance();
  await redisManager.connect();
  const redisClient = redisManager.getClient();
  
  // Инициализация репозиториев
  const userRepo = new UserRepository(redisClient);
  const productRepo = new ProductRepository(redisClient);
  
  // Инициализация движка рекомендаций
  const recommendationEngine = new RedisRecommendationEngine(
    redisClient,
    userRepo,
    productRepo
  );
  
  // Инициализация контроллера
  const recommendationController = new RecommendationController(
    recommendationEngine,
    productRepo
  );
  
  // Настройка Express
  const app = express();
  app.use(express.json());
  
  // Маршруты
  app.get(
    '/users/:userId/recommendations', 
    recommendationController.getRecommendations.bind(recommendationController)
  );
  
  app.post(
    '/users/:userId/products/:productId/view',
    recommendationController.trackProductView.bind(recommendationController)
  );
  
  app.post(
    '/users/:userId/products/:productId/purchase',
    recommendationController.trackProductPurchase.bind(recommendationController)
  );
  
  // Запуск сервера
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Сервис рекомендаций запущен на порту ${PORT}`);
  });
  
  // Обработка завершения
  process.on('SIGTERM', async () => {
    console.log('Получен сигнал завершения, закрываем соединения...');
    await redisManager.disconnect();
    process.exit(0);
  });
}
 
bootstrap().catch(err => {
  console.error('Ошибка запуска сервиса:', err);
  process.exit(1);
});

Детальный разбор архитектурных решений и компромиссов



Первое, на что стоит обратить внимание — выбор комбинированного паттерна кеширования (Cache-Aside + Write-Through). Этот подход дает нам сбалансированное решение: Cache-Aside минимизирует начальную задержку при холодном старте, а Write-Through гарантирует актуальность данных. Однако за это мы платим двойными операциями записи, что может создавать дополнительную нагрузку при массовых обновлениях. В моем опыте, если система испытывает больше чтений, чем записей (как в большинстве рекомендательных систем), то этот компромис оправдан.

Использование сортированных множеств (Sorted Sets) для хранения рекомендаций — еще одно неочевидное, но мощное решение. Сортированное множество идеально подходит для рейтингования элементов, обеспечивая логорифмическую сложность O(log n) для большинства операций. Однако я упустил важный аспект: при изменении алгоритма подсчета рейтинга потребуется полное пересчитывание всех скоров. Возможное решение — хранить "версию алгоритма" вместе с каждым набором рекомендаций, но это усложняет логику.

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

Относительно TTL (время жизни) для рекомендаций — мы выбрали 30 минут. Почему именно так? В реальных системах я обнаружил, что рекомендации имеют "срок свежести", который обычно измеряется десятками минут для активных пользователей. Слишком долгий TTL приводит к неактуальным рекомендациям, слишком короткий — к лишней нагрузке.

Сравнивая альтернативы Redis, стоит упомянуть KeyDB и Dragonfly. KeyDB — это форк Redis с многопоточностью и лучшей поддержкой больших обьемов данных, что могло бы повысить пропускную способность нашей системы на мощных серверах. Dragonfly, с другой стороны, предлагает модель "из коробки" для многопоточной обработки команд, оптимизированную для современных многоядерных систем. Мои тесты показывают, что при более чем 8 ядрах Dragonfly может дать прирост до 30% в многопользовательских сценариях по сравнению с ванильным Redis. Однако ни KeyDB, ни Dragonfly не имеет такой обширной экосистемы и проверенной временем стабильности как Redis. Выбор зависит от конкретных требований: если нужна максимальная пропускная способность на многоядерных системах — стоит рассмотреть альтернативы; если важнее стабильность и консистентность — Redis остается золотым стандартом.

Socket.io & Redis
Ребята, здарова! Я сделал адаптер задал адаптер для socket.io - socket.io-redis - как написано в...

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

Кука уходит в Redis, но не уходит в браузер
Здравствуйте, уважаемые форумчане! Никак не могу понять, что не так. Раньше все работало. Я создаю...

Создать редактор радиосхем для MVC5, используя TypeScript
Ребята!) Нужна инфа как возможно создать редактор,используя typescript (js нежелательно),на mvc 5...

Как писать тесты для typeorm + typegraphql + typescript
Добрый день. написал небольшое приложение. Оно работает и вроде все нормально. суть...

Функциональное программирование для typescript не работает
Здравствуйте. Вот в 3ех разных файлах Здесь js как в примере из инета. Так работает. Хорошо ли,...

TypeScript vs Script# vs
У кого какой опыт ? - сравнительные достоинства и недостатки.

Перевод C# на TypeScript
Доброго времени суток))) (Извините если не в ту тему) Существует рабочая программы для локального...

VS2012 + typescript 9.1.1
При работе с TypeScript VS2012 виснет или закрывается регулярно, никакой конкретной информации об...

Разбиение скомилированного Typescript на файлы
В проекте имеется множество typescript файлов, которые компилируются в один js. Но часть скриптов...

Изучение TypeScript - советы
Нуждаюсь в срочном освоении TypeScript. Поделитесь ресурсами, пожалуйста. Можно на русском и...

Не понятен пример кода из спецификации TypeScript
Читаю про объектные типы в спецификации на странцие 13, но не понятно из описания как устроен и...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru