Otimize seu Aplicativo com Cache de Dados usando NestJS e Redis

iamjose

José Paulo Marinho

Posted on April 13, 2024

Otimize seu Aplicativo com Cache de Dados usando NestJS e Redis

No mundo de desenvolvimento de software, construir aplicações robustas e performáticas são de suma importância.

Pense no seguinte cenário: digamos que você tenha um aplicação de consulta de usuários, e toda vez que alguém entra no perfil de determinado usuário, é preciso realizar uma consulta no banco de dados, certo? E aí vai retornar dados como:

nome, data de nascimento, estado civil, altura, peso, formação, etc.

Raramente essas informações vão mudar, certo? Como evitar que seu servidor consulte sempre o banco de dados?

A resposta é simples: CACHE!

A técnica de cache se baseia no princípio de armazenar temporariamente dados frequentemente acessados em uma área de armazenamento de acesso mais rápido, como a memória RAM, para reduzir o tempo necessário para recuperar esses dados em comparação com a obtenção dos dados diretamente da fonte original, como um banco de dados ou uma API externa.

Vamos utilizar uma ótima solução para realizar essa técnica, o Redis.

Redis é um armazenamento de estrutura de dados em memória, usado como um banco de dados em memória distribuído de chave-valor, cache e agente de mensagens, com durabilidade opcional extremamente rápido.

Desenho da arquitetura

Inicialmente sem cache, sua modelagem fica no seguinte modelo:

Arquitetura Microserviço sem cache

Após o cache, ficará assim:

Arquitetura Microserviço com cache

Para realizar o cache é necessário realizar pelo menos uma ou mais vezes consulta no banco de dados, nunca dá pra saber quando o usuário vai querer mudar seus dados, então, quando o servidor realizar uma consulta no Banco de Dados (3), ele irá armazenar esses dados no Redis (4).

A partir desse momento você poderá consultar no Redis os dados:

Desenho arquitetura obtendo do redis

  • Consulta do Redis, se não encontrou os dados:

    • Consulta Banco de Dados
    • Armazena os dados no Redis
    • retorna os dados
  • Consulta do Redis, se encontrou os dados:

    • retorna os dados

Iniciando o Projeto

Para esse exemplo, estarei utilizando uma aplicação NestJS + Redis + MongoDB, mas você pode realizar com outros frameworks, como Java utilizando Spring Data Redis + H2, ou GoLang utilizando go-redis + POSTGRES, entre muitas outras.

Para iniciar um aplicativo NestJS, é necessário ter instalado o CLI do Nest:

$ npm install -g @nestjs/cli

Para criar um projeto:

nest new cache-with-redis

Estarei utilizando o Mongoose, como TypeORM para se conectar ao Banco de dados MongoDB.

$ npm i @nestjs/mongoose mongoose

Para o redis, estarei utilizando um Redis Módulo para o Nest:

$ npm install @liaoliaots/nestjs-redis ioredis

Para configurar o projeto utilizando variáveis de ambiente, estarei instalando um módulo do Nest para configuração:

$ npm i --save @nestjs/config

Vamos iniciar configurando um módulo para o Redis, comece criando 2 arquivos:

redis-cache.module.ts
redis-repository.ts

// redis-cache.module.ts
import { RedisModule } from "@liaoliaots/nestjs-redis";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { RedisCacheRepository } from "./redis-cache.repository";

@Module({
    imports: [
        ConfigModule.forRoot(),
        RedisModule.forRoot({
            config: {
                host: process.env.REDIS_HOST,
                port: Number(process.env.REDIS_PORT),
                password: process.env.REDIS_PASSWORD
            }
        })
    ],
    providers: [RedisCacheRepository],
    exports: [RedisCacheRepository]
})
export class CacheRedisModule {}
Enter fullscreen mode Exit fullscreen mode
// redis-cache.repository.ts
import { InjectRedis } from "@liaoliaots/nestjs-redis";
import { Injectable } from "@nestjs/common";
import Redis from 'ioredis';

@Injectable()
export class RedisCacheRepository {
    constructor(@InjectRedis() private readonly redis: Redis) {}

    async saveData<T>(data: T, key: string): Promise<void> {
        // key -> chave onde o redis irá salvar os dados
        // JSON.stringify(data) -> salvar os dados em JSON
        // EX -> utilizado para indicar que o tempo será passado em segundos
        // 180 -> 180 segundos = 3 minutos de cache
        // Time Complexity -> O(1)
        await this.redis.set(key, JSON.stringify(data), "EX", 180)
    }

    async getData<T>(key: string): Promise<T> {
        // retorna o dado salvo da chave
        // Time Complexity -> O(1)
        return JSON.parse(await this.redis.get(key)) as T;
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora iremos cuidar da nossa entidade Usuário e seu repositório utilizando MongoDB, crie 5 arquivos:

models/user.entity.ts
user.repository.ts
user.servive.ts
user.controller.ts
user.module.ts

// user.entity.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";

export type UserDocument = HydratedDocument<User>;

export enum GenderEnum {
    FEMALE = "Female",
    MALE = "Male"
}

export enum MaritalStatusEnum {
    SINGLE = "Single",
    MARRIED = "Married",
    Divorced = "Divorced",
    Widowed = "Widowed",
    Separated = "Separated"
}

@Schema()
export class User {

    @Prop()
    name: string;

    @Prop()
    age: number;

    @Prop({ type: String, enum: GenderEnum })
    gender: GenderEnum

    @Prop({ type: String, enum: MaritalStatusEnum })
    maritalStatus: MaritalStatusEnum;

    @Prop()
    height: number;
}

export const UserSchema = SchemaFactory.createForClass(User);


Enter fullscreen mode Exit fullscreen mode
// user.repository.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { User } from "./models/user.entity";
import { Model } from "mongoose";
import { CreateUserDto } from "./dto/create-user.dto";

@Injectable()
export class UserRepository {
    constructor(@InjectModel(User.name) private readonly userModel: Model<User>) {}

    async create(createUserDto: CreateUserDto): Promise<User> {
        const userCreated = new this.userModel(createUserDto);
        return userCreated.save();
    }

    async findById(userId: string): Promise<User> {
        return await this.userModel.findById(userId);
    }
}
Enter fullscreen mode Exit fullscreen mode
// user.servive.ts
import { Injectable } from "@nestjs/common";
import { UserRepository } from "./user.repository";
import { RedisCacheRepository } from "src/redis-cache/redis-cache.repository";
import { CreateUserDto } from "./dto/create-user.dto";

@Injectable()
export class UserService {
    constructor(
        private readonly userRepository: UserRepository,
        private readonly redisCacheRepository: RedisCacheRepository
    ) {}

    async getUserById(userId: string) {
        const userCache = await this.redisCacheRepository.getData(userId);

        if (userCache) {
            return userCache;
        }

        const userRepository = await this.userRepository.findById(userId);

        await this.redisCacheRepository.saveData(userRepository._id, userRepository);

        return userRepository;
    }

    async saveUser(userCreateDto: CreateUserDto) {
        return await this.userRepository.create(userCreateDto);
    }
}
Enter fullscreen mode Exit fullscreen mode
// user.controller.ts
import { Body, Controller, Get, HttpStatus, Param, Post, Res } from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dto/create-user.dto";
import { Response } from "express";

@Controller("user")
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post()
    async postUser(@Body() userDto: CreateUserDto, @Res() res: Response) {
        const userCreated = await this.userService.saveUser(userDto);

        res.status(HttpStatus.OK).json(userCreated);
    }

    @Get(":id")
    async get(@Param("id") id: string, @Res() res: Response) {
        const user = await this.userService.getUserById(id);

        res.status(HttpStatus.OK).json(user);
    }
}
Enter fullscreen mode Exit fullscreen mode

É importante criar um arquivo .env no root do projeto:

# .env
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=password
MONGODB_HOST="mongodb://127.0.0.1:27017/CACHE_WITH_REDIS"
Enter fullscreen mode Exit fullscreen mode

Pronto, a aplicação está pronta para ser executada. Você pode executar executando o comando:

$ npm run start

Logo após verá um saída dessa forma:

Log Running App

Para salvar um usuário, podemos realizar uma requisição do tipo POST com o curl:

curl --location 'localhost:3000/user' \
--header 'Content-Type: application/json' \
--data '{
  "name": "John Doe",
  "age": 30,
  "gender": "Male",
  "maritalStatus": "Married",
  "height": 1.80
}'
Enter fullscreen mode Exit fullscreen mode

Logo após retornará algo do gênero:

Response create user

Redis em Ação

Agora veremos o Redis em Ação, ao realizar uma requisição do método GET para obter o usuário:

curl --location 'localhost:3000/user/6619fa598d1a2e8cb93a95f3'
Enter fullscreen mode Exit fullscreen mode

Vamos obter o seguinte resultado:

Response get user by id

Repare que a resposta volta com o seguinte tempo de resposta: 34ms. De acordo com nossa regra de negócio, ele faz uma consulta no Redis, se não achar, ele vai no Banco de Dados(MongoDB), logo após salva no redis e retorna, certo? Boa, vamos ver se salvou no redis?

# Redis CLI
> GET 6619fa598d1a2e8cb93a95f3
"{\"_id\":\"6619fa598d1a2e8cb93a95f3\",\"name\":\"John Doe\",\"age\":30,\"gender\":\"Male\",\"maritalStatus\":\"Married\",\"height\":1.8,\"__v\":0}"
Enter fullscreen mode Exit fullscreen mode

Olha que legal, agora o nosso dado está salvo no Redis, vamos ver se melhorou o tempo de resposta da API?

Response get user by id with cache

4ms, isso é um absurdo de rápido. Você deve estar pensando: "Caramba, só 29ms de diferença, isso não faz diferença."
Em ambientes escaláveis e robustos que necessitam de sistemas rápidos, como o PIX, qualquer diferença de tempo muda. Imagine quando você precisar cachear o resultado de alguma API externa em que você utiliza que não muda muito o resultado, seria de grande utilidade, certo?

Chegamos ao final. Espero que tenham gostado, estarei deixando alguns links de referência:

Configuration NestJS
MongoDB NestJS
Redis Module NestJS
SET command Redis
GET command Redis

O link do repositório utilizado no tutorial se encontra aqui:
Projeto Repositório

Um Abraço e bons estudos! Até mais!

💖 💪 🙅 🚩
iamjose
José Paulo Marinho

Posted on April 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related