Arquitetura assíncrona PubSub com Redis e Node.js

Node.js

Arquitetura assíncrona PubSub com Redis e Node.js

Luiz Duarte
Escrito por Luiz Duarte em 26/10/2021
Junte-se a mais de 22 mil profissionais de TI

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Um caso de uso de Node.js muito comum, como já expliquei em outros artigos antes, é o de construção de webapis e microsserviços que aguentam grande carga de requisições. Até aqui, isso não deve ser novidade para você.

No entanto, uma fraqueza do Node (e do JavaScript em geral) é a sua performance não tão boa quando o assunto é processamento pesado e/ou tarefas bloqueantes que demoram um tempo razoável para serem concluídas. Isso no Node.js pode ser um grande ofensor uma vez que o event loop trabalha com single thread, certo? Bloqueie esta thread principal e os demais clientes chamando sua API terão uma experiência bem ruim…

Seja nos casos em que o volume de requisições exceda a sua capacidade de resolvê-las rapidamente ou nos casos em que o processamento seja demorado, adotar uma arquitetura que opere de forma assíncrona não apenas garante que você vai conseguir atender todas requisições como vai fazê-lo em um tempo adequado.

No tutorial de hoje vou falar de como construir uma arquitetura simples, porém robusta, usando Node.js e Redis para processamento assíncrono de requisições recebidas em uma web API RESTful.

Processamento Assíncrono

O primeiro conceito que você tem de entender é que apesar do HTTP ser síncrono, já faz quase 20 anos que a Internet (e os sistemas que rodam nela) já entendeu que trabalhar de maneira assíncrona é uma forma de proporcionar experiências cada vez melhores aos usuários.

Em uma requisição síncrona, a request é enviada ao servidor, que a processa e devolve uma response. Tudo de uma vez só. Enquanto a response não é retornada, a conexão fica presa e o usuário fica esperando. Se demorar demais, a conexão pode ser encerrada abruptamente e o cliente terá de fazê-la de novo, sem saber exatamente o que acontecer nesta segunda chamada.

Em uma requisição assíncrona, a request é enviada ao servidor, que registra a mesma e automaticamente responde ao cliente que vai realizar a tarefa em breve, avisando-o de alguma maneira quando ela for concluída. Um outro processo cuida de processar essa requisição armazenada e notificar o cliente de alguma forma, se necessário.

A imagem abaixo ilustra um pouco dessa diferença.

Sync vs Async

Obviamente o segundo modelo é um pouco mais complexo de lidar, mas possui algumas vantagens muito interessantes.

Enquanto que processamento síncrono dá uma resposta mais direta e rápida para o cliente quando o servidor não está sobrecarregado de requisições, é quando a carga de chamadas às suas APIs é muito alta que ele se mostra inviável. Neste caso, apenas registrar as requisições, para processá-las em uma fila, por exemplo, garante que todos vão ser atendidos, cada um no seu tempo e nenhuma request vai ser dropada.

Assim, pensando nessa arquitetura, eu proponho neste tutorial o uso de Redis, uma tecnologia poderosíssima que pode ser usada como fila para as suas requisições, visando processamento assíncrono pelo Node.js. Mais do que isso, implementaremos um padrão chamado PubSub, onde um elemento que seja publicado na fila (publish) possa ser consumido por diversos assinantes (subscribers).

O que é Redis?

Redis é um “banco de dados” (não confundir com SGBD) in-memory e open-source, usado geralmente como cache ou como message broker (tipo RabbitMQ e AWS SQS). Ele fornece estruturas de dados como strings, hashes, listas, conjuntos, conjuntos ordenados, bitmaps, índices geoespaciais, streams, entre outros.

Apesar de ser muito associado como “banco em memória”, Redis tem diferentes níveis de persistência, transações, replicação e permite fazer clusterização. E apesar também de ser muito associado a estruturas chave-valor, ele permite uma grande gama de operações diferentes do que apenas consultar e armazenar baseado em chaves.

Neste tutorial usaremos ele como message broker, ou seja, como uma ferramenta que irá receber mensagens, vai enfileirar elas e depois vai entregar uma a uma aos destinatários. Isso garante que nenhuma requisição seja perdida, pois tudo que chegar será registrado, e que conforme as tarefas vão sendo executadas, os requisitantes vão sendo notificados. Isso pode ser feito na relação 1×1, que seria uma fila comum sendo consumida por um worker (processador de tarefas), como abaixo.

Producer/Consumer

Ou na relação de 1:n, que costumamos chamar de PubSub, onde temos um único publisher (alguém que recebe e registra as requisições no canal) e vários subscribers (destinatários ou workers diferentes) que estão esperando por mensagens, como mostra a arquitetura abaixo.

Em nosso exemplo, o Redis será o canal ali no meio, uma web API Node.js será o publisher e os subscribers podem ser workers escritos em qualquer tecnologia que converse com Redis. Faremos aqui com Node.js, por praticidade.

Mas indo ao que interessa, baixe a versão stable no site oficial e rode o comando ‘make’ na pasta raiz para compilar ele na sua máquina e depois um ‘make test’ para ver se está tudo ok. Isso te deixará apto a rodar um servidor de Redis localmente usando o seguinte comando a partir da pasta do Redis.

Isso vai subir uma instância do Redis com as configurações padrões, que são o suficiente para este tutorial.

Se você estiver no Windows, segue vídeo com tutorial de instalação.

Curso FullStack

Como usar com Node.js?

Para uso com Node.js você irá precisar de uma aplicação Node.js básica que você pode criar rapidamente aí na sua máquina e de uma biblioteca de uso do Redis para Node, como a Node Redis. No entanto, a Node Redis não possui suporte a Promises ainda, então vou recomendar que você instale outra lib, a Promise Redis. Também aproveitarei para instalar o Express junto, pois vamos precisar mais à frente.

Esse pacote promise-redis é um wrapper do pacote Node Redis que apenas lhe dá os poderes das promises. A Node Redis por sua vez, faz uma transcrição literal em JavaScript de TODOS os comandos possíveis de serem executados em um servidor Redis, mas aqui vamos fazer um uso bem simples dele.

Assim, no fim das contas, estaremos usando Node Redis, mas com suporte a Promises, o que é algo bem importante para estarmos escrevendo JavaScript mais moderno. Na verdade, meu interesse maior é de usar Async/Await, que depende da existência de Promises sendo retornadas pelas funções.

Abaixo, um exemplo simples de cliente que se conecta em um Redis padrão (localhost sem usuário e senha), e expõe duas funções, uma para publicar mensagens em um canal e outra para assinar mensagens de um canal. Chamei este módulo de queue.js:

Aquele objeto client expõe todos os comandos existentes no Redis, amplamente documentados e que recomendo que você dê uma olhada caso queira ir além do publish e subscribe que usei ali, que são o básico do básico do Redis como message broker.

Eu criei este módulo genérico e separado para que possamos utilizá-lo tanto pelo publisher quanto pelo subscriber. Nosso publisher será uma web API Node.js e será muito simples, uma vez que o objetivo aqui não é ensinar a criar webapis. Apenas copie e cole o código abaixo em um arquivo publisher.js na raiz do projeto:

Repare como tenho duas rotas POST que fazem a mesma coisa, mas em canais diferentes. Se você rodar esta web api com node publisher.js verá que ele recebe requisições normalmente se testar via Postman ou outro HTTP client. Essas requisições terão seus corpos enviados para o Redis como string (único formato suportado pela função publish).

Mas e agora, como fazemos os workers que vão consumir estes canais 1 e 2?

Curso Node.js e MongoDB

Subscribers/Workers

Da mesma forma, nosso worker de exemplo será muito simples, uma vez que não é o foco do artigo. Apenas copie e cole o código abaixo em um arquivo subscriber1.js na raiz do projeto:

Esse worker é muito simples, ele apenas fica escutando ao canal channel1 (a mesma em que o publisher vai jogar as mensagens) e quando chega alguma coisa lá, o callback pega a mensagem e apenas imprime o conteúdo no console. Você inclusive pode criar cópias deste worker para fazer testes mais elaborados de PubSub.

Uma vez que você já tenha o servidor de Redis rodando, testar é muito simples, basta subir a publisher.js via terminal, nossa web API, e depois subir um ou mais subscribers.js em outro terminal, em qualquer ordem.

Você deve começar o teste pelo publisher, ou seja, abra o POSTMAN e envie um objeto JSON qualquer via POST para localhost:3000/channel1 ou channel2 que isso deve disparar a mensagem pra fila que deve ser consumida pelos subcribers que estiverem ouvindo quase imediatamente.

Claro, este é um subscriber meeega simples. Subscribers reais vão processar dados da mensagem, fazer operações em banco e até mesmo chamar outras APIs se necessário, principalmente para avisar que essa requisição já foi processada. Você pode querer também alterar a interface para dar uma resposta ao usuário e por aí vai.

Um ponto importante: uma vez que use um cliente de Redis como publisher ou subscriber, você não conseguirá usar ele para outras atividades. Isso quer dizer que, se você quiser pegar/enviar dados ao Redis (principalmente dentro do callback de mensagem recebida), terá de inicializar outro cliente. Esse é um erro bem comum e passei muita raiva até entender, vai ter um erro de “can’t get in this context” ou algo assim.

Mas enfim, a ideia deste artigo era te ajudar a fazer o Redis funcionar como PubSub com Node.js e espero que você tenha conseguido. Caso contrário, apenas baixe os meus fontes usando o formulário ao final do tutorial.

Dois concorrentes do Redis para esta finalidade é o RabbitMQ e o AWS SQS.

Espero que tenha gostado do tutorial.

Um abraço e até a próxima!

Para mais lições como essa, conheça meus livros e meus cursos, tenho certeza que vai gostar!

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.