Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Node.js: gerenciando threads e processos

Node.js: gerenciando threads e processos

Trabalhando com Streams e Banco de Dados - Apresentação 

Boas-vindas ao curso de Gerenciamento de Threads e Processos com o Node.js! Eu sou o Thiago Bussola e serei seu instrutor.

Audiodescrição: Thiago é um homem branco de cabelos e barba curtos na cor castanho-escuro. Usa óculos de grau com armação quadrada e uma camiseta preta. Ao fundo, há uma estante com livros e objetos de decoração.

Este curso é destinado a quem já possui experiência no desenvolvimento de aplicações em Node.js e deseja aprender a criar scripts que utilizem melhor a concorrência e o paralelismo. Além de entender como funciona o loop de eventos e o ciclo interno do Node.js.

O que vamos aprender

Neste curso, você aprenderá a usar strings para ler e inserir dados no banco de dados, além de entender como funciona o loop de eventos no Node.js.

Vamos explorar o uso de threads e processos, aprendendo a utilizar worker threads para tarefas intensivas de CPU. Também veremos como usar o módulo Spawn para chamar subprocessos e o Child Process para inserir informações no banco de dados de maneira eficiente.

Pré-requisitos

Para tirar o máximo proveito deste conteúdo, é importante que você tenha concluído o curso anterior sobre Node.js. Você deve ter experiência em desenvolver APIs REST com NEST ou outro framework e um conhecimento básico sobre manipulação de arquivos com Node.js.

Aproveite os recursos da plataforma, que incluem vídeos, atividades, e o apoio do fórum e da nossa comunidade no Discord. Vamos estudar!

Trabalhando com Streams e Banco de Dados - Criando uma base de dados com Faker

Neste vídeo, vamos criar uma base de dados para trabalharmos ao longo do curso. Para isso, é necessário que você tenha realizado a atividade Preparando Ambiente e que tenha configurado um banco de dados Postgres. Isso pode ser feito com Docker ou com o Postgres instalado localmente, além de um gerenciador de banco de dados, como o Postgres, Beekeeper ou MySQL Workbench.

Com tudo preparado, vamos criar uma tabela que simulará, por exemplo, a tabela de usuários de um e-commerce ou de outro site que possua uma tabela de usuários. Nesse caso, já temos tudo pronto. Na nossa model, podemos ver todos os campos que essa tabela de usuários contemplará, desde o campo de senha até o nome da pessoa.

Para começar, acessamos a raiz do projeto e na pasta "src", clicamos com o botão direito e depois tem "New Folder" para criar uma nova pasta chamada "seed". Nessa pasta criamos um arquivo chamado seed.ts. Nele, faremos alguns imports necessários para a conexão com o banco de dados e o modelo de usuário, começando com o connectDB e o closeDB. Depois, importamos o User e o faker.

import { closeDB, connectDB } from "../db/connection";
import User from "../db/user.model";
import { faker } from "@faker-js/faker";

Gerando dados fictícios

Criamos uma função generateUser(), que retornará um objeto gerado com o faker. Este objeto incluirá todos os campos presentes na model. Por exemplo, o campo name será gerado com faker.internet.username, e o campo company com faker.company.name.

Lembrando que é importante que os nomes dos objetos sejam iguais aos que estão na model.

Na linha abaixo passamos o dateBirth: faker.date.past(), seguido de password, que nesse caso receberá um faker. O faker tem uma propriedade para simular senhas, mas para as atividades futuras, queremos que estejam com o texto puro para que depois possamos criptografá-las. Então, passamos faker.internet.username() + faker.company.name().

Na sequência, escrevemos createdAt: faker.date.past({}), abrindo função e objeto. Podemos passar alguns parâmetros, por exemplo, queremos os usuários criados até dez anos atrás, então passamos years: 10, refDate: new Date().

Feito isso, copiamos as três ultimas linhas de código que criamos e colamos logo abaixo. Feito isso, mudamos de createdAt para updatedAt e mudamos os anos para 9. Fazemos o mesmo abaixo, mudando para lastPasswordUpdateAt, ficando da seguinte forma:

function generateUser() {
  return {
    name: faker.internet.username(),
    company: faker.company.name(),
    dateBirth: faker.date.past(),
    password: faker.internet.username() + faker.company.name(),
    createdAt: faker.date.past({
      years: 10,
      refDate: new Date(),
    }),
    updatedAt: faker.date.past({
      years: 9,
      refDate: new Date(),
    }),
    lastPasswordUpdateAt: faker.date.past({
      years: 8,
      refDate: new Date(),
    }),
  };
}

Criando a função de inserção de usuários

Agora, vamos criar nossa função para inserir esses usuários. A função será async function seedUsers({}). Podemos utilizar um bloco try-catch para tratar possíveis erros durante a inserção.

async function seedUsers() {
   try {
     for (let i = 0; i < 200_000; i++) {
       const user = generateUser();
       await User.create(user);
     }
        console.log("Usuários inseridos")
   } catch (error) {
     console.error("Erro ao cadastrar usuários", error);
   }
 }

Para executar nossa função, criaremos uma closure, que é uma função autoexecutável que chamará outras funções do nosso código. Ela conectará primeiro, depois inserirá no banco e, por fim, fechará a conexão. Passamos (async () => {}, depois, dentro da função, escrevemos await connectDB().

Para medirmos o tempo de execução, passamos console.time("seed-db"), seguido da função await seedUsers(). Por fim, passamos console.timeEnd("seed-db") e await closeDB().

(async () => {
  await connectDB();
  console.time("seed-db");
  await seedUsers();
  console.timeEnd("seed-db");
  await closeDB();
})();

Rodando o script no terminal

Vamos salvar, abrir o terminal e executar o comando para rodar o script. Como estamos usando TypeScript, o comando será:

npx ts-node-dev src/seed/seed.ts

Utilizamos o Beekeeper como gerenciador de banco de dados. Vamos atualizar e verificar a tabela de usuários, onde os usuários estão sendo inseridos. A tabela já foi criada. Vamos rodar um script, que estará disponível na descrição do vídeo, para contar quantos usuários estão sendo inseridos.

SELECT COUNT(*) AS total_records FROM users

Avaliando o tempo de execução

Agora, 200 mil usuários foram inseridos. No VS Code, notamos que a execução demorou 2 minutos e 24 segundos, sendo 2 minutos e 23 para inserir os registros no banco. Isso ocorre porque estamos inserindo usuários sequencialmente, um de cada vez. Precisamos esperar uma promessa ser resolvida antes de inserir no banco. No próximo vídeo, vamos explorar algumas abordagens para melhorar o tempo de inserção.

Trabalhando com Streams e Banco de Dados - Melhorias na inserção de dados ​

Como vimos no vídeo anterior, utilizamos uma abordagem mais interativa. Chamamos a função awaitUserCreate 200 mil vezes, o que demorou dois minutos. Se aumentarmos para um milhão de usuários, pode demorar de 15 a 16 minutos. Podemos ajustar o número de usuários conforme necessário, mas é possível melhorar o tempo de inserção para criar nossa base de dados.

Vamos abrir o Beekeeper Studio e remover a tabela atual, pois vamos gerar outra. Em seguida, voltamos ao código e comentamos da linha 35 até a linha 26. Na linha 35, quebramos uma linha duas vezes e, na linha 37, colamos a função novamente. Agora, faremos algumas pequenas modificações para melhorar o tempo de escrita.

Para melhorar a eficiência, precisamos modificar a abordagem. Na linha 37, dentro da função, criamos uma constante chamada constBatchSize, que representa o tamanho do lote. Queremos inserir os dados em lotes de mil usuários.

const batchSize = 1000;

Assim, o for continua o mesmo, exceto pela última parte, onde o i incrementa de mil em mil até chegar aos 200 mil.

for (let i = 0; i < 200_000; i += 1000)

Ajustando a inserção com Promise.all

Podemos apagar as linhas 42 e 43 para deixar o for mais limpo. Em seguida, criamos constBatchRecebeArray.from, especificando que o array terá o tamanho do lote. Como parâmetro, passamos uma função anônima, generateUser, para que cada índice do array seja um usuário gerado pela função criada anteriormente.

Chamamos awaitPromise.all e passamos batch.map, com uma função anônima que chama user.createUser para cada usuário específico. Podemos adicionar um console.log para mostrar o tamanho do lote inserido, usando interpolação para exibir "usuários inseridos".

async function seedUsers() {
    const batchSize = 1000;

    try {
        for (let i = 0; i < 200_000; i += 1000) {
            const batch = Array.from({ length: batchSize }, () => generateUser());
            await Promise.all(batch.map((user) => User.create(user)));
            console.log(`${i + batchSize} usuarios inseridos`);
        }

        console.log("Usuários inseridos com sucesso");
    } catch (error) {
        console.error("Erro ao cadastrar usuários", error);
    }
}

Após salvar, podemos executar o código. Anteriormente, o processo demorou cerca de dois minutos. Vamos verificar quanto tempo levará agora.

A função utilizada é a mesma de antes. Podemos usar o comando npx ts-node-dev src/seed.ts. Ao executar, a conexão é feita e os dados começam a ser inseridos em uma velocidade satisfatória. No Beekeeper, ao atualizar a tabela, observamos que foi criada corretamente e os dados estão sendo inseridos rapidamente. Com essa abordagem, conseguimos inserir dados de forma mais rápida.

Concorrência

Vamos entender o motivo disso. Ao usar await Promise.all, lidamos com um array de promessas resolvidas de forma concorrente. Isso não significa paralelismo, mas sim concorrência. Podemos explorar essa diferença em vídeos futuros. Basicamente, o método inicia a resolução de uma promessa e, antes de finalizar, já começa outra em paralelo. Quando todas são concluídas, ele retorna os resultados, evitando a inserção sequencial e tornando o processo mais eficiente.

Utilizando BulkCreate para otimizar

O tempo de execução foi de 38 segundos, o que representa um ganho significativo em relação aos 2 minutos anteriores. Ao verificar novamente, confirmamos que os 200 mil usuários foram inseridos. Vamos apagar a tabela e explorar uma forma de melhorar essa abordagem. Podemos comentar o await Promise.all e, como estamos usando o Sequelize, utilizar a função BulkCreate. Para usar o BulkCreate, basta chamar await User.bulkCreate e passar o lote de dados.

await User.bulkCreate(batch);

Após salvar, executamos novamente. A inserção foi concluída em 10 segundos, comparado aos 40 segundos anteriores e aos 2 minutos iniciais. O BulkCreate insere dados em lotes de mil, enquanto o Promise.all resolve promessas em paralelo, mas ainda uma de cada vez. O BulkCreate insere diretamente no banco de dados, resultando em um ganho de performance significativo, cerca de quatro vezes mais rápido que a abordagem anterior.

Essa técnica é recomendada para gerar bases de dados ou realizar migrações. Se o ORM utilizado suporta o BulkCreate, é uma excelente abordagem. Caso contrário, a abordagem com Promise.all ainda oferece um ganho de performance em comparação a inserir dados um por um em um loop.

Sobre o curso Node.js: gerenciando threads e processos

O curso Node.js: gerenciando threads e processos possui 168 minutos de vídeos, em um total de 51 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas