José Paulo Marinho
Posted on April 13, 2024
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:
Após o cache, ficará assim:
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:
-
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 {}
// 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;
}
}
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);
// 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);
}
}
// 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);
}
}
// 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);
}
}
É 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"
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:
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
}'
Logo após retornará algo do gênero:
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'
Vamos obter o seguinte resultado:
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}"
Olha que legal, agora o nosso dado está salvo no Redis, vamos ver se melhorou o tempo de resposta da API?
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!
Posted on April 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.