Использование клиента 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 в свое собственное приложение, оптимизируя его производительность и улучшая управление данными.