DEV Community

Cover image for Otimize seu Aplicativo com Cache de Dados usando NestJS e Redis
José Paulo Marinho
José Paulo Marinho

Posted on • Edited on

14 2 2 2 2

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!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (1)

Collapse
 
paulo_henrique_47837fd122 profile image
Paulo Henrique

show

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs