Другое

Redis кэширование в TypeORM: Полное руководство

Узнайте, как настроить Redis как драйвер кэширования для TypeORM. Повышайте производительность приложений, уменьшая нагрузку на базу данных. Пошаговое руководство с примерами кода.

Как использовать Redis как драйвер для кэширования запросов в TypeORM?

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

Содержание

Настройка окружения и установка зависимостей

Для начала работы с Redis кэшированием в TypeORM необходимо установить несколько ключевых пакетов:

bash
npm install redis typeorm reflect-metadata class-validator
# или для yarn
yarn add redis typeorm reflect-metadata class-validator

Основные зависимости включают:

  • redis - клиент для работы с Redis сервером
  • typeorm - основной ORM фреймворк
  • reflect-metadata - для декораторов TypeScript
  • class-validator - для валидации данных (опционально)

После установки зависимостей убедитесь, что у вас установлен Redis сервер. Для разработки можно использовать Docker:

bash
docker run -d -p 6379:6379 redis:alpine

Проверьте, что Redis работает, выполнив команду в терминале:

bash
redis-cli ping

Должен вернуться ответ “PONG”.

Конфигурация Redis соединения

Создайте файл конфигурации для Redis соединения. Обычно это делается в отдельном файле конфигурации (например, redis.config.ts):

typescript
// src/config/redis.config.ts
import { RedisOptions } from 'ioredis';

export const redisOptions: RedisOptions = {
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || undefined,
  db: parseInt(process.env.REDIS_DB || '0'),
  retryDelayOnFailover: 100,
  maxRetriesPerRequest: 3,
  lazyConnect: true,
  enableReadyCheck: true,
  keepAlive: true,
  reconnectOnError: (err) => {
    const targetError = 'READONLY';
    if (err.message.includes(targetError)) {
      return true;
    }
    return false;
  },
};

Для работы с TypeScript создайте интерфейс конфигурации:

typescript
// src/types/redis.types.ts
export interface RedisCacheOptions {
  host: string;
  port: number;
  password?: string;
  db?: number;
  keyPrefix?: string;
  ttl?: number;
  isGlobal?: boolean;
}

Настройка TypeORM с Redis кэшированием

Для настройки TypeORM с Redis кэшированием необходимо создать специальный кэш-провайдер. Создайте файл redis-cache.ts:

typescript
// src/cache/redis-cache.ts
import { Cache, CacheOptions } from 'typeorm';
import Redis from 'ioredis';
import { redisOptions } from '../config/redis.config';

export class RedisCache implements Cache {
  private redis: Redis;
  private options: CacheOptions;

  constructor(options: CacheOptions) {
    this.options = options;
    this.redis = new Redis(redisOptions);
  }

  async get(key: string): Promise<any> {
    const value = await this.redis.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set(key: string, value: any, seconds?: number): Promise<void> {
    const ttl = seconds || this.options.ttl || 3600;
    await this.redis.setex(key, ttl, JSON.stringify(value));
  }

  async remove(key: string): Promise<void> {
    await this.redis.del(key);
  }

  async clear(): Promise<void> {
    await this.redis.flushdb();
  }

  async disconnect(): Promise<void> {
    await this.redis.quit();
  }
}

Теперь настройте TypeORM для использования Redis кэша:

typescript
// src/config/typeorm.config.ts
import { DataSource } from 'typeorm';
import { RedisCache } from '../cache/redis-cache';

export const AppDataSource = new DataSource({
  type: 'postgres', // или другой тип вашей базы данных
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER || 'postgres',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'mydb',
  entities: [__dirname + '/../entities/**/*.entity{.ts,.js}'],
  migrations: [__dirname + '/../migrations/**/*{.ts,.js}'],
  subscribers: [__dirname + '/../subscribers/**/*{.ts,.js}'],
  cache: {
    type: 'redis',
    options: {
      cache: new RedisCache({}),
      // дополнительные опции
      duration: 60000, // время жизни кэша в миллисекундах
      alwaysEnabled: true,
    }
  },
  synchronize: process.env.NODE_ENV !== 'production',
  logging: process.env.NODE_ENV === 'development',
});

Для использования в приложении:

typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppDataSource } from './config/typeorm.config';
import { RedisCache } from './cache/redis-cache';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...AppDataSource.options,
      cache: {
        type: 'redis',
        options: {
          cache: new RedisCache({}),
          duration: 60000,
        }
      }
    }),
  ],
})
export class AppModule {}

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

Базовое кэширование запросов

Включите кэширование для конкретной сущности:

typescript
// src/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
@Cache({ type: 'redis', duration: 300000 }) // 5 минут
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  createdAt: Date;
}

Используйте кэширование в репозитории:

typescript
// src/services/user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { Cache } from 'typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  @Cache({ type: 'redis', key: 'user::#{id}' })
  async findById(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  @Cache({ type: 'redis', key: 'users::#{page}-#{limit}' })
  async findAll(page: number = 1, limit: number = 10): Promise<[User[], number]> {
    return this.userRepository.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
    });
  }

  // Метод без кэширования
  async create(userData: Partial<User>): Promise<User> {
    const user = this.userRepository.create(userData);
    return this.userRepository.save(user);
  }
}

Кэширование с пользовательскими ключами

Для более сложных сценариев создайте кастомный декоратор кэширования:

typescript
// src/decorators/cache.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const CACHE_KEY = 'cache_key';
export const CACHE_OPTIONS = 'cache_options';

export const CacheWithKey = (key: string, options?: any) => 
  (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    SetMetadata(CACHE_KEY, key)(target, propertyKey, descriptor);
    SetMetadata(CACHE_OPTIONS, options)(target, propertyKey, descriptor);
  };

Используйте в сервисе:

typescript
// src/services/product.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from '../entities/product.entity';
import { CacheWithKey } from '../decorators/cache.decorator';

@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product)
    private productRepository: Repository<Product>,
  ) {}

  @CacheWithKey('product::category::#{categoryId}', { type: 'redis', duration: 600000 })
  async findByCategory(categoryId: number): Promise<Product[]> {
    return this.productRepository.find({ 
      where: { categoryId },
      order: { createdAt: 'DESC' }
    });
  }
}

Очистка кэша

Для очистки кэша при изменении данных:

typescript
// src/services/user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { Cache } from 'typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async updateUser(id: number, userData: Partial<User>): Promise<User> {
    const user = await this.findById(id);
    Object.assign(user, userData);
    const updatedUser = await this.userRepository.save(user);
    
    // Очистка кэша для обновленного пользователя
    await this.userRepository.manager.connection.cache.remove(
      `user::${id}`
    );
    
    return updatedUser;
  }
}

Оптимизация производительности

Настройка TTL (Time To Live)

Для различных типов данных используйте разное время жизни кэша:

typescript
// src/config/cache.config.ts
export const CACHE_TTL = {
  USER_PROFILE: 3600,        // 1 час
  USER_LIST: 1800,           // 30 минут
  PRODUCT_DETAILS: 7200,     // 2 часа
  SEARCH_RESULTS: 900,       // 15 минут
  STATISTICS: 300,           // 5 минут
};

Пакетная обработка данных

Для улучшения производительности используйте пакетную вставку данных с кэшированием:

typescript
// src/services/batch.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BatchItem } from '../entities/batch-item.entity';
import { Cache } from 'typeorm';

@Injectable()
export class BatchService {
  constructor(
    @InjectRepository(BatchItem)
    private batchItemRepository: Repository<BatchItem>,
  ) {}

  @Cache({ type: 'redis', key: 'batch::status::#{batchId}' })
  async getBatchStatus(batchId: string): Promise<any> {
    const items = await this.batchItemRepository.find({
      where: { batchId },
      select: ['id', 'status', 'processedAt']
    });
    
    return {
      batchId,
      totalItems: items.length,
      processedItems: items.filter(i => i.status === 'processed').length,
      status: items.length > 0 ? items[0].status : 'pending'
    };
  }

  async processBatch(batchId: string, items: any[]): Promise<void> {
    const batchEntities = items.map(item => 
      this.batchItemRepository.create({
        batchId,
        ...item,
        status: 'pending'
      })
    );
    
    await this.batchItemRepository.save(batchEntities);
    
    // Очистка кэша после добавления новых данных
    await this.batchItemRepository.manager.connection.cache.clear();
  }
}

Мониторинг производительности

Добавьте мониторинг для отслеживания эффективности кэширования:

typescript
// src/monitoring/cache.monitor.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CacheMetrics } from '../entities/cache-metrics.entity';

@Injectable()
export class CacheMonitorService {
  constructor(
    @InjectRepository(CacheMetrics)
    private cacheMetricsRepository: Repository<CacheMetrics>,
  ) {}

  async recordCacheHit(operation: string, duration: number): Promise<void> {
    await this.cacheMetricsRepository.save({
      operation,
      type: 'hit',
      duration,
      timestamp: new Date()
    });
  }

  async recordCacheMiss(operation: string, duration: number): Promise<void> {
    await this.cacheMetricsRepository.save({
      operation,
      type: 'miss',
      duration,
      timestamp: new Date()
    });
  }

  async getCacheStats(operation?: string): Promise<any> {
    const conditions = operation ? { operation } : {};
    
    const hits = await this.cacheMetricsRepository.count({
      where: { ...conditions, type: 'hit' }
    });
    
    const misses = await this.cacheMetricsRepository.count({
      where: { ...conditions, type: 'miss' }
    });
    
    const total = hits + misses;
    const hitRate = total > 0 ? (hits / total) * 100 : 0;
    
    return {
      hits,
      misses,
      total,
      hitRate: hitRate.toFixed(2) + '%'
    };
  }
}

Отладка и мониторинг

Включение логирования Redis

Для отладки включите подробное логирование Redis:

typescript
// src/config/redis-debug.config.ts
import { RedisOptions } from 'ioredis';

export const redisDebugOptions: RedisOptions = {
  ...redisOptions,
  retryDelayOnFailover: 100,
  maxRetriesPerRequest: 3,
  lazyConnect: true,
  enableReadyCheck: true,
  keepAlive: true,
  reconnectOnError: (err) => {
    const targetError = 'READONLY';
    if (err.message.includes(targetError)) {
      return true;
    }
    return false;
  },
  // Включаем логирование
  showFriendlyErrorStack: true,
  enableReadyCheck: false,
  connectTimeout: 10000,
  commandTimeout: 5000,
};

// Создаем расширенный Redis клиент с логированием
export class DebugRedisCache extends RedisCache {
  constructor(options: CacheOptions) {
    super(options);
    this.redis.on('error', (err) => {
      console.error('Redis Error:', err);
    });
    this.redis.on('connect', () => {
      console.log('Redis connected successfully');
    });
    this.redis.on('ready', () => {
      console.log('Redis is ready');
    });
    this.redis.on('reconnecting', () => {
      console.log('Redis reconnecting...');
    });
    this.redis.on('close', () => {
      console.log('Redis connection closed');
    });
  }
}

Инструменты мониторинга

Используйте следующие инструменты для мониторинга Redis кэша:

  1. Redis CLI - для базовой проверки:
bash
redis-cli info memory
redis-cli info stats
redis-cli keys "*typeorm*"
  1. RedisInsight - визуальный инструмент для мониторинга Redis

  2. Prometheus + Grafana - для комплексного мониторинга:

yaml
# prometheus.yml
scrape_configs:
  - job_name: 'redis'
    static_configs:
      - targets: ['redis:6379']
    metrics_path: '/metrics'
    scrape_interval: 15s

Профилирование производительности

Добавьте профилирование для измерения производительности кэширования:

typescript
// src/utils/performance.utils.ts
import { performance } from 'perf_hooks';

export class PerformanceProfiler {
  private static timers = new Map<string, number>();

  static start(key: string): void {
    this.timers.set(key, performance.now());
  }

  static end(key: string): number {
    const startTime = this.timers.get(key);
    if (!startTime) return 0;
    
    const duration = performance.now() - startTime;
    this.timers.delete(key);
    return duration;
  }

  static async measureAsync<T>(key: string, fn: () => Promise<T>): Promise<{ result: T; duration: number }> {
    this.start(key);
    const result = await fn();
    const duration = this.end(key);
    return { result, duration };
  }
}

// Использование в сервисе
async getUserWithProfiling(id: number) {
  return PerformanceProfiler.measureAsync(
    `getUser_${id}`,
    () => this.findById(id)
  );
}

Заключение

Использование Redis в качестве драйвера для кэширования запросов в TypeORM предоставляет значительные преимущества для производительности приложений. Вот ключевые выводы:

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

  2. Гибкая конфигурация - TypeORM позволяет настраивать различные параметры кэширования, включая TTL, префиксы ключей и стратегии инвалидации, что позволяет адаптировать кэширование под конкретные нужды приложения.

  3. Масштабируемость - Redis кластеры обеспечивают горизонтальное масштабирование кэша, что позволяет обрабатывать большие объемы данных и высокую нагрузку.

  4. Надежность - Встроенные механизмы восстановления Redis и возможность репликации данных обеспечивают высокую доступность кэша.

  5. Простота интеграции - Интеграция с TypeORM осуществляется через стандартный интерфейс кэширования, что упрощает внедрение и поддержку.

Для оптимальной работы с Redis кэшированием рекомендуется:

  • Регулярно анализировать метрики производительности
  • Настраивать подходящие значения TTL для разных типов данных
  • Внедрать мониторинг и алерты для оперативного реагирования на проблемы
  • Проводить нагрузочное тестирование для определения оптимальной конфигурации

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

Источники

  1. Официальная документация TypeORM - Caching
  2. Redis Cache Integration with TypeORM - Tutorial
  3. Redis Performance Best Practices
  4. TypeORM Cache Interface Implementation
  5. NestJS TypeORM Integration Guide
Авторы
Проверено модерацией
Модерация