NestJS та Redis у 2026: Кешування, Сесії та Питання на Співбесідах

Повний посібник з інтеграції Redis у NestJS. Cache-manager, управління сесіями та стратегії інвалідації кешу.

NestJS та Redis у 2026: Кешування, Сесії та Питання на Співбесідах

Інтеграція Redis з NestJS трансформує продуктивність застосунку, замінюючи запити до бази даних швидкими зверненнями до пам'яті. Комбінація @nestjs/cache-manager для кешування та express-session з connect-redis для збереження сесій охоплює два найпоширеніших випадки використання Redis у бекенд-застосунках.

Ключовий висновок

Кешування Redis у NestJS зменшує навантаження на базу даних, зберігаючи часто використовувані дані в пам'яті. Управління сесіями з Redis дозволяє горизонтальне масштабування завдяки спільному використанню стану сесій між кількома екземплярами.

Налаштування @nestjs/cache-manager з Redis

Модуль cache-manager був винесений в окремий пакет у NestJS 10. Встановлення вимагає як обгортки NestJS, так і базового сховища кешу. Адаптер @keyv/redis є рекомендованим підходом для інтеграції Redis з cache-manager v5.

bash
# Install required packages
npm install @nestjs/cache-manager cache-manager @keyv/redis

Реєстрація модуля налаштовує з'єднання з Redis та TTL за замовчуванням. Опція isGlobal робить кеш доступним у всіх модулях без повторного імпорту.

app.module.tstypescript
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: () => ({
        stores: [
          new KeyvRedis(process.env.REDIS_URL || 'redis://localhost:6379'),
        ],
        ttl: 60000, // Default TTL in milliseconds
      }),
    }),
  ],
})
export class AppModule {}

Масив stores приймає кілька сховищ кешу для багаторівневого кешування. Продакшн-середовища зазвичай використовують Redis як основне сховище з опціональним fallback до локальної пам'яті для розробки.

Впровадження Кешу та Кешування на Рівні Сервісу

Токен CACHE_MANAGER забезпечує прямий доступ до кешу для операцій на рівні сервісу. Цей підхід пропонує більше контролю, ніж автоматичне HTTP-кешування, коли бізнес-логіка визначає інвалідацію кешу.

users.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { UsersRepository } from './users.repository';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
    private usersRepository: UsersRepository,
  ) {}

  async findById(id: string): Promise<User | null> {
    // Check cache first
    const cacheKey = `user:${id}`;
    const cached = await this.cache.get<User>(cacheKey);
    
    if (cached) {
      return cached;
    }

    // Cache miss: fetch from database
    const user = await this.usersRepository.findById(id);
    
    if (user) {
      await this.cache.set(cacheKey, user, 300000); // 5 minutes TTL
    }

    return user;
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const user = await this.usersRepository.update(id, data);
    
    // Invalidate cache on update
    await this.cache.del(`user:${id}`);
    
    return user;
  }
}

Патерн ключа кешу user:{id} дозволяє цільову інвалідацію. Складніші патерни на кшталт user:* вимагають команд, специфічних для Redis, через базовий клієнт.

CacheInterceptor для Автоматичного Кешування HTTP-відповідей

Вбудований CacheInterceptor автоматично кешує HTTP GET-відповіді. Застосування на рівні контролера кешує всі GET-ендпоїнти, тоді як застосування на рівні методу забезпечує гранулярний контроль.

products.controller.tstypescript
import { Controller, Get, UseInterceptors, Param } from '@nestjs/common';
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';

@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get()
  @CacheTTL(120000) // Override default TTL: 2 minutes
  async findAll() {
    return this.productsService.findAll();
  }

  @Get(':id')
  @CacheKey('product-detail') // Custom cache key prefix
  async findOne(@Param('id') id: string) {
    return this.productsService.findById(id);
  }
}

Інтерцептор за замовчуванням генерує ключі кешу з URL запиту. Кастомні декоратори @CacheKey() перевизначають цю поведінку для маршрутів, де параметри запиту не повинні впливати на кешування. Концепції інтерцепторів та гардів у NestJS часто з'являються на технічних співбесідах.

Колізії ключів кешу

Ключ кешу за замовчуванням використовує повний URL, включаючи параметри запиту. Два запити до /products?page=1 та /products?page=2 створюють окремі записи в кеші. Перевизначте за допомогою @CacheKey(), коли пагінація не повинна створювати дубльовані кешовані відповіді.

Управління Сесіями з Redis та express-session

NestJS за замовчуванням працює на Express, що робить express-session стандартним вибором для управління сесіями. Адаптер connect-redis зберігає дані сесій у Redis замість пам'яті, забезпечуючи збереження сесій під час перезапуску сервера.

bash
# Install session packages
npm install express-session connect-redis redis
npm install -D @types/express-session

Конфігурація сесій відбувається в main.ts перед запуском застосунку. Redis-клієнт з'єднується незалежно від налаштування cache-manager.

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Initialize Redis client
  const redisClient = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379',
  });
  await redisClient.connect();

  // Configure session middleware
  app.use(
    session({
      store: new RedisStore({ client: redisClient }),
      secret: process.env.SESSION_SECRET || 'change-this-in-production',
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        maxAge: 24 * 60 * 60 * 1000, // 24 hours
      },
    }),
  );

  await app.listen(3000);
}
bootstrap();

Опція saveUninitialized: false запобігає збереженню порожніх сесій, зменшуючи використання пам'яті Redis. Налаштування secure: true для кукі вимагає HTTPS у продакшні.

Готовий до співбесід з Node.js / NestJS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Доступ до Сесій у Контролерах та Гардах

Дані сесії доступні через об'єкт request. Типізований інтерфейс сесії покращує досвід розробника та виявляє помилки під час компіляції.

session.interface.tstypescript
export interface SessionData {
  userId?: string;
  email?: string;
  roles?: string[];
  loginAt?: Date;
}

declare module 'express-session' {
  interface SessionData {
    userId?: string;
    email?: string;
    roles?: string[];
    loginAt?: Date;
  }
}

Розширений тип сесії робить властивості сесії доступними з автодоповненням.

auth.controller.tstypescript
import { Controller, Post, Body, Req, HttpCode } from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  @HttpCode(200)
  async login(@Body() loginDto: LoginDto, @Req() req: Request) {
    const user = await this.authService.validateUser(
      loginDto.email,
      loginDto.password,
    );

    // Store user data in session
    req.session.userId = user.id;
    req.session.email = user.email;
    req.session.roles = user.roles;
    req.session.loginAt = new Date();

    return { message: 'Logged in successfully' };
  }

  @Post('logout')
  @HttpCode(200)
  async logout(@Req() req: Request) {
    return new Promise((resolve, reject) => {
      req.session.destroy((err) => {
        if (err) reject(err);
        resolve({ message: 'Logged out successfully' });
      });
    });
  }
}

Гард захищає маршрути, перевіряючи валідність сесії. Цей патерн плавно інтегрується з системою авторизації та RBAC NestJS.

session.guard.tstypescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class SessionGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    return !!request.session?.userId;
  }
}

Стратегії Інвалідації Кешу в Застосунках NestJS

Інвалідація кешу — це складніша проблема в кешуванні. Три стратегії застосовуються до різних сценаріїв:

Закінчення часу (TTL) працює для даних, які змінюються передбачувано, або коли незначна застарілість прийнятна. Каталоги продуктів, значення конфігурації та публічний контент відповідають цьому патерну.

Інвалідація на основі подій очищає записи кешу, коли базові дані змінюються. Це вимагає явних викликів cache.del() в операціях оновлення та видалення.

Інвалідація на основі патернів видаляє кілька пов'язаних ключів. Redis підтримує видалення за патерном через команди SCAN та DEL, але cache-manager абстрагує це.

cache-invalidation.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';

@Injectable()
export class CacheInvalidationService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  // Single key invalidation
  async invalidateUser(userId: string): Promise<void> {
    await this.cache.del(`user:${userId}`);
  }

  // Multiple related keys
  async invalidateUserRelated(userId: string): Promise<void> {
    const keys = [
      `user:${userId}`,
      `user:${userId}:profile`,
      `user:${userId}:preferences`,
    ];
    
    await Promise.all(keys.map((key) => this.cache.del(key)));
  }

  // Clear entire cache (use sparingly)
  async clearAll(): Promise<void> {
    await this.cache.reset();
  }
}

Метод reset() очищає всі кешовані дані, корисний під час розгортань або міграцій даних, але небезпечний у продакшні при випадковому виклику.

Пулінг З'єднань Redis та Підтримка Кластера

Продакшн-розгортання вимагають пулінгу з'єднань для ефективної обробки паралельних запитів. Бібліотека ioredis надає підтримку кластера та пулінг з'єднань одразу.

redis.config.tstypescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis, { Cluster } from 'ioredis';

@Injectable()
export class RedisConfig {
  constructor(private configService: ConfigService) {}

  createClient(): Redis | Cluster {
    const clusterNodes = this.configService.get<string>('REDIS_CLUSTER_NODES');

    if (clusterNodes) {
      // Cluster mode
      const nodes = clusterNodes.split(',').map((node) => {
        const [host, port] = node.split(':');
        return { host, port: parseInt(port, 10) };
      });

      return new Redis.Cluster(nodes, {
        redisOptions: {
          password: this.configService.get('REDIS_PASSWORD'),
        },
      });
    }

    // Standalone mode
    return new Redis({
      host: this.configService.get('REDIS_HOST', 'localhost'),
      port: this.configService.get('REDIS_PORT', 6379),
      password: this.configService.get('REDIS_PASSWORD'),
      maxRetriesPerRequest: 3,
    });
  }
}

Для застосунків, що використовують TypeORM або Prisma, з'єднання з базою даних та кешем мають бути налаштовані окремо з власними пулами з'єднань.

Питання на Співбесідах з NestJS та Redis

Технічні співбесіди з кешування Node.js охоплюють як концептуальне розуміння, так і практичну реалізацію. Ці питання зустрічаються часто:

Чому використовувати Redis замість кешування в пам'яті?

Кешування в пам'яті не зберігається між перезапусками і не може бути спільним між кількома екземплярами. Redis забезпечує персистентність, реплікацію та спільний стан для горизонтально масштабованих застосунків. Компроміс — це мережева затримка порівняно з доступом до локальної пам'яті.

Чим cache-manager відрізняється від прямого використання Redis?

Бібліотека cache-manager надає уніфікований API для різних сховищ кешу. Перехід з пам'яті на Redis вимагає зміни конфігурації сховища без модифікації коду сервісу. Пряме використання Redis пропонує більше Redis-специфічних можливостей, як pub/sub, sorted sets та скрипти Lua.

Що таке cache stampede і як цьому запобігти?

Cache stampede виникає, коли популярний запис кешу закінчується і багато паралельних запитів одночасно звертаються до бази даних. Стратегії запобігання включають:

  • Оновлення з блокуванням: Один запит оновлює кеш, поки інші чекають
  • Ймовірнісне раннє закінчення: Випадкове оновлення до закінчення TTL
  • Фонове оновлення: Окремий процес оновлює записи, що закінчуються

Коли використовувати CacheInterceptor проти кешування на рівні сервісу?

CacheInterceptor підходить для stateless GET-ендпоїнтів, де відповідь залежить лише від URL. Кешування на рівні сервісу обробляє сценарії, де інвалідація кешу має відбуватися при зміні даних або коли операції, відмінні від GET, потребують кешування.

Як обробляти атаки session fixation з Redis-сесіями?

Потрібно регенерувати ID сесії після автентифікації. Виклик req.session.regenerate() перед збереженням даних користувача запобігає використанню зловмисниками ID сесії до автентифікації.

typescript
// Secure login with session regeneration
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
  const user = await this.authService.validateUser(
    loginDto.email,
    loginDto.password,
  );

  return new Promise((resolve, reject) => {
    req.session.regenerate((err) => {
      if (err) reject(err);
      
      req.session.userId = user.id;
      req.session.email = user.email;
      resolve({ message: 'Logged in successfully' });
    });
  });
}
Порада для співбесіди

Сеньйорні кандидати пояснюють компроміси між стратегіями кешування. Джуніор може сказати "кешування покращує продуктивність". Сеньйор пояснює, коли кешування додає складність без користі, наприклад, для високо персоналізованих або швидко змінюваних даних.

Моніторинг Продуктивності та Метрики Redis

Продакшн-застосунки потребують видимості продуктивності кешу. Ключові метрики включають:

  • Hit rate: Відсоток запитів, обслужених з кешу. Нижче 80% вказує на проблеми з TTL або стратегією ключів.
  • Використання пам'яті: Команда Redis INFO memory показує поточне та пікове використання пам'яті.
  • Кількість з'єднань: Занадто багато з'єднань вказує на відсутність пулінгу.
  • Кількість виселень: Ненульові виселення означають, що кеш заповнений і видаляє записи.
redis-health.service.tstypescript
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisHealthService {
  constructor(private redis: Redis) {}

  async getMetrics() {
    const info = await this.redis.info('stats');
    const memory = await this.redis.info('memory');
    
    const stats = this.parseInfo(info);
    const mem = this.parseInfo(memory);

    const hits = parseInt(stats.keyspace_hits, 10);
    const misses = parseInt(stats.keyspace_misses, 10);
    const hitRate = hits / (hits + misses) || 0;

    return {
      hitRate: (hitRate * 100).toFixed(2) + '%',
      usedMemory: mem.used_memory_human,
      connectedClients: stats.connected_clients,
      evictedKeys: stats.evicted_keys,
    };
  }

  private parseInfo(info: string): Record<string, string> {
    return info.split('\n').reduce((acc, line) => {
      const [key, value] = line.split(':');
      if (key && value) acc[key.trim()] = value.trim();
      return acc;
    }, {} as Record<string, string>);
  }
}

Варто експонувати ці метрики через health check ендпоїнт або інтегрувати з Prometheus для продакшн-моніторингу.

Redis-кешування та Сесії в Продакшн-розгортаннях NestJS

Розгортання NestJS з Redis вимагає уваги до кількох операційних аспектів:

  • Управління connection string: Зберігайте URL Redis у змінних середовища, не в коді. Використовуйте secrets management для паролів.
  • Graceful shutdown: Закривайте з'єднання Redis під час зупинки застосунку, щоб запобігти витоку з'єднань.
  • Логіка повторних спроб: Налаштуйте exponential backoff для помилок з'єднання Redis.
  • Окремі екземпляри Redis: Розгляньте окремі екземпляри Redis для кешу та сесій. Виселення кешу не повинно впливати на дані сесій.
graceful-shutdown.tstypescript
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisCleanup implements OnModuleDestroy {
  constructor(private redis: Redis) {}

  async onModuleDestroy() {
    await this.redis.quit();
  }
}

Для мікросервісних архітектур Redis також слугує брокером повідомлень через вбудований транспортний шар NestJS, об'єднуючи кешування, сесії та міжсервісну комунікацію.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Щоденний виклик

Чи знайдеш ти помилку в Node.js / NestJS?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 10 вересня 2026 р.

Поділитися

Пов'язані статті