Использование клиента Redis в NestJS
В этой статье мы рассмотрим, как настроить клиент Redis в приложении NestJS для кэширования и хранения кратковременных данных. Мы предоставим фрагменты кода и пояснения для каждого этапа процесса.
Настройка клиента Redis
Сначала нам нужно установить Nest CLI, выполнив следующую команду в вашем терминале:
npm install -g @nestjs/cli
Затем создайте новый проект NestJS, используя следующую команду, заменив <your-project-name>
ее желаемым именем проекта:
nest new <your-project-name>
Этот проект будет сосредоточен на двух основных целях Redis: кэшировании и хранении кратковременных данных. Мы будем использовать Node.js версии 18 и ioredis
библиотеку для подключения к сервису Redis.
Настройка фабрики клиентов Redis
Чтобы взаимодействовать с Redis, нам нужно настроить клиентскую фабрику Redis, которая создаст экземпляр соединения Redis, используемый во всем коде. Вот пример клиентской фабрики Redis:
import { FactoryProvider } from '@nestjs/common'; import { Redis } from 'ioredis'; export const redisClientFactory: FactoryProvider<Redis> = { provide: 'RedisClient', useFactory: () => { const redisInstance = new Redis({ host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, }); redisInstance.on('error', e => { throw new Error(`Redis connection failed: ${e}`); }); return redisInstance; }, inject: [], };
Обратите внимание, что в этом примере мы не реализуем какой-либо механизм повтора соединения. Если возникнет проблема с соединением Redis, будет выдана ошибка, что, возможно, не является лучшей практикой, но подходит для нашего варианта использования.
Настройка репозитория Redis
Мы настроим репозиторий Redis для абстрагирования нашего клиента Redis и предоставим методы для взаимодействия с хранилищем Redis на уровне приложения. Вот пример репозитория Redis:
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common'; import { Redis } from 'ioredis'; import { RedisRepositoryInterface } from '../../../domain/interface/redis.repository.interface'; @Injectable() export class RedisRepository implements OnModuleDestroy, RedisRepositoryInterface { constructor(@Inject('RedisClient') private readonly redisClient: Redis) {} onModuleDestroy(): void { this.redisClient.disconnect(); } async get(prefix: string, key: string): Promise<string | null> { return this.redisClient.get(`${prefix}:${key}`); } async set(prefix: string, key: string, value: string): Promise<void> { await this.redisClient.set(`${prefix}:${key}`, value); } async delete(prefix: string, key: string): Promise<void> { await this.redisClient.del(`${prefix}:${key}`); } async setWithExpiry(prefix: string, key: string, value: string, expiry: number): Promise<void> { await this.redisClient.set(`${prefix}:${key}`, value, 'EX', expiry); } }
Мы используем метод событий жизненного цикла NestJS onModuleDestroy
, чтобы закрыть соединение Redis при закрытии приложения. Это событие запускается при app.close()
вызове или когда процесс получает специальный системный сигнал, например SIGTERM
, если enableShutdownHooks
вызывается во время начальной загрузки приложения.
Как видите, мы используем префикс для всех ключей, хранящихся в Redis. Это позволяет нам создавать структуру, подобную папкам, при доступе к хранилищу Redis с помощью таких приложений, как RedisInsight, используя двоеточие (':') в качестве разделителя. Префикс определяется как значение перечисления, содержащее все хранилища данных (коллекции/таблицы), используемые в проекте.
Настройка службы Redis
Наконец, мы создаем службу Redis для использования на уровне приложения. Служба Redis инкапсулирует репозиторий Redis и предоставляет методы, необходимые на уровне приложения. Вот пример службы Redis:
import { Inject, Injectable } from '@nestjs/common'; import { RedisPrefixEnum } from '../domain/enum/redis-prefix-enum'; import { ProductInterface } from '../domain/interface/product.interface'; import { RedisRepository } from '../infrastructure/redis/repository/redis.repository'; const oneDayInSeconds = 60 * 60 * 24; const tenMinutesInSeconds = 60 * 10; @Injectable() export class RedisService { constructor(@Inject(RedisRepository) private readonly redisRepository: RedisRepository) {} async saveProduct(productId: string, productData: ProductInterface): Promise<void> { // Expiry is set to 1 day await this.redisRepository.setWithExpiry( RedisPrefixEnum.PRODUCT, productId, JSON.stringify(productData), oneDayInSeconds, ); } async getProduct(productId: string): Promise<ProductInterface | null> { const product = await this.redisRepository.get(RedisPrefixEnum.PRODUCT, productId); return JSON.parse(product); } async saveResetToken(userId: string, token: string): Promise<void> { // Expiry is set to 10 minutes await this.redisRepository.setWithExpiry( RedisPrefixEnum.RESET_TOKEN, token, userId, tenMinutesInSeconds, ); } async getResetToken(token: string): Promise<string> { return await this.redisRepository.get(RedisPrefixEnum.RESET_TOKEN, token); } }
В этом сервисе мы используем RedisPrefixEnum
в качестве имени коллекции/таблицы. Мы реализовали четыре метода: saveProduct
кэшировать продукт на день, saveResetToken
хранить токен сброса в течение десяти минут и getProduct
получать getResetToken
сохраненные значения продукта и токена соответственно.
Использование службы Redis в других службах
Теперь давайте посмотрим, как мы можем использовать службу Redis в нашей службе сброса пароля и службе продукта.
Служба сброса пароля
Вот пример службы сброса пароля с использованием службы Redis:
import { Inject, Injectable } from '@nestjs/common'; import { ResetTokenInterface } from '../domain/interface/reset.token.interface'; import { RedisService } from './redis.service'; @Injectable() export class PasswordResetService { constructor(@Inject(RedisService) private readonly redisService: RedisService) {} async generateResetToken(userId: string): Promise<ResetTokenInterface> { // Check if the user exists in the database // Generate a random number token with a length of 6 const token = Math.floor(100000 + Math.random() * 900000).toString(); await this.redisService.saveResetToken(userId, token); return { token }; } async getTokenUserId(token: string): Promise<string | null> { return await this.redisService.getResetToken(token); } }
Обслуживание продукта
Вот пример сервиса продукта, использующего сервис Redis:
import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { ProductInterface } from '../domain/interface/product.interface'; import { RedisService } from './redis.service'; // We will be using a dummy JSON product API to fetch our product data // This could be any API call or database operation const productURL = 'https://dummyjson.com/products/'; @Injectable() export class ProductService { constructor(@Inject(RedisService) private readonly redisService: RedisService) {} async getProduct(productId: string): Promise<any> { // Check if the product exists in Redis const product = await this.redisService.getProduct(productId); if (product) { console.log('Cache hit! Product found in Redis'); return { data: product }; } const res = await fetch(`${productURL}${productId}`); if (res.ok) { const product: ProductInterface = await res.json(); // Cache the data in Redis await this.redisService.saveProduct(`${product.id}`, product); console.log('Cache miss! Product not found in Redis'); return product; } else { throw new InternalServerErrorException('Something went wrong'); } } }
Эти службы могут быть внедрены в другие службы или контроллеры.
Заключение
В этой статье мы рассмотрели, как настроить клиент Redis в приложении NestJS для кэширования и хранения кратковременных данных. Мы создали клиентскую фабрику Redis, репозиторий Redis и службу Redis для абстрагирования взаимодействий Redis на уровне приложения. Мы также продемонстрировали, как использовать службу Redis для сброса пароля и службы продукта для улучшения поиска и кэширования данных.
Реализуя методы и структуры, описанные в этой статье, вы можете легко включить Redis в свое собственное приложение, оптимизируя его производительность и улучшая управление данными.