segunda-feira, 20 de outubro de 2025

Idempotência - A necessidade de garantir integridade em chamadas distribuídas

        Idempotência significa que executar a mesma operação várias vezes tem o mesmo efeito que executá-la uma única vez. Para escrever um CRUD que não permita entradas duplicadas por falha de rede, reenvio automático, duplicidade, etc é preciso atenção na implementação e usar alguns padrões. 

        Podemos usar Chaves NaturaisQuando um recurso já possui um identificador natural único (como CPF, CNPJ, número de contrato, e-mail, matrícula etc.), podemos utilizá-lo como chave principal no cadastro. Isso elimina a possibilidade de duplicidade no nível lógico do sistema.

        Controle de Duplicidade no Banco de Dados: está bem ligada com a primeira opção, já que quando usamos chaves naturais costumamos controlar via banco como PK ou pelo menos unique constraint. Mas aqui também pode estrapolar esse conceito e usar outros campos em constraints, criandopadrões que não podem se repetir. Dependendo do caso uma data ou outro campo podem ser usados para criar essa verificação no Banco de Dados, e ao tentar inserir os dois registros irá barrar.

        Outra opção usada é a “Idempotency Key” (chave única por requisição). O cliente gera uma chave única (ex: UUID) e a envia no cabeçalho da requisição, então o servidor armazena o resultado da primeira requisição associada a essa chave e se a mesma chave for usada novamente, o servidor retorna o mesmo resultado, sem processar a operação novamente.

      Onde essa chave é armazenada? No Banco de Dados ou Cache Distribuído (Redis, Memcached, etc.). Dependendo da aplicação, pode-se guardar nos 2 locais. Uma terceira opção seria a sessão do usuário, para aplicações web autenticadas.

        Para o caso de clique nervoso do usuário, desabilitar o botão no momento do clique até que volte uma resposta também pode ajudar. Isso também vai depender da situação, do sistema, etc., mas sim, para alguns casos, desabilitar o botão enquanto o servidor responde pode resolver muita coisa. 



sábado, 18 de outubro de 2025

Outbox Pattern - Salve antes de qualquer coisa!

        Em sistemas distribuídos, nos micro serviços, algumas transações abrangem vários serviços, e é comum (e recomendado) que cada um deles tenha seu Banco de Dados, e sua forma de tratar a informação.

            O padrão Outbox nada mais é do que salvar os dados em uma tabela antes de enviar para o próximo passo. Desta maneira, caso ocorra uma falha no serviço que tem a mensagem antes dele repassar, os dados estão garantidos. Depois um JOB faz a leitura dos dados e vai enviando para o brocker ou fazendo o que tiver que fazer, podendo tentar novamente quantas vezes necessário. 

       Caso ocorra algum problema, os dados estão no banco para serem reprocessados. Após o processamento ocorrer com sucesso, os dados podem ser apagados com segurança!

            Isso também serve para aliviar a carga do microsserviço, por exemplo, uma API que tem que receber os dados em grande volume e tratá-lo pra enviar para os próximos serviços, poderia receber e guardar rapidamente no Banco de Dados e então um JOB em segundo plano faz essa parte, estando o processo da API livre para próxima requisição.

        Trabalhei em um sistema onde determinada API recebia os dados, um lote de até mil contas, gerava o Tracer ID, e um ID de lote, validava uns 5 campos de cabeçalho, salvava no banco, depois fazia o repasse para o broken, que então o microsserviço consumia, pegava o lote, dividia conta a conta fazendo validações primárias, e enviava para os microsserviços (eram 2, um de cada área da empresa) fazerem o devido processamento conta a conta. Após o processamento de cada conta, cada microsserviço de área, enviava para o microsserviço de resposta o resultado, que era atualizado no banco de dados (aquele primeiro, usado pela API para guardar a entrada). Lá, depois de todo lote processado, o status do lote era alterado para a situação correspondente (sucesso, sucesso parcial, falha, etc). A qualquer momento poderia se consultar o status do lote na API. Nesse caso, os dados da tabela de entrada tinham controle de status e não eram apagados. A necessidade de apagar ou não, varia de acordo com o negócio. Pode ser que seja interessante manter ou não, em outros quem sabe manter por uns dias apenas?

        Pra esse artigo é só! 

sexta-feira, 17 de outubro de 2025

Aplicações Distribuídas - Usando lock distribuído para gerenciar processamento de dados

        Um dos motivos de eu escrever esse Blog, na verdade o principal deles, é eu ter conteúdo pra mim mesmo de uma forma rápida e fácil, pois na nossa área tem coisas que fazemos e depois esquecemos, ou vemos em algum lugar e quando precisamos não conseguimos encontrar. Esse é um desses casos. Essa publicação surgiu de uma palestra que vi do Rafael Ponte no canal da Zup Innovation no youtube que deixo o link no final e aconselho bastante que assistam. Aqui é apenas um resumo para consulta rápida.

         Um problema comum quando trabalhamos com a aplicações distribuídas é quando temos um JOB ou algo do tipo que precisa realizar um determinado processamento de dados e precisamos garantir que esse processamento seja único para cada registro ou linha da tabela.

        Em uma aplicação com uma única instância podemos fazer isso com o synchronized do Java e vida que segue, mas, hoje normalmente trabalhamos com mais de uma instância da aplicação e aí as coisas complicam um pouco. O synchronized resolve para uma única instância mas não tem controle sobre outras execuções, o que nos faz ter que buscar outras soluções.

        Se os dados que precisam ser processados podem ser processados por uma única instância sem criar gargálos, temos por exemplo soluções como:

  •         Eleger um servidor como responsável por executar esse JOB. Essa eu já usei e é bem comum. Pode-se ter uma tabela de parâmetros no banco com o IP de um servidor específico e o JOB checa se o IP cadastrado é o daquela instância, caso seja, ele executa. Pode-se ter uma propriedade no banco (não necessariamente o IP) e outra com mesmo valor em um arquivo de config, ao ler e comparar as duas, sendo iguais, o servidor executa.
  • Usar o próprio banco de dados para gerenciar o lock. Assim, o próprio banco locka os dados enquanto estiverem sendo usados naquela transação. Esse modo basta adicionar uma anotação na consulta:


    
   

        No caso, diretamente no SQL ficaria assim:


        Lembrando que, é preciso garantir que os métos e a atualização sejam eficientes para não locar os registros por muito tempo. Ou seja, o uso do @Transaction precisa ser feito de forma eficiente, ficando apenas a parte necessária da operação dentro da transação. Na Aula o Rafael até mostra uma forma de como fazer isso também.


        Agora, quando os dados precisam ser processados de forma paralela por mais de uma máquina, temos outras soluções a considerar.

  • Uma máquina lê os dados e distribui para as outras processarem. Dependendo do volume de dados e outras variáveis, essa solução era muito usada antes das mensagerias. Uma máquina lê os dados e chama uma outra máquina de uma lista de máquinas disóníveis para executar o processamento. Por exemplo, uma companhia telefônica que precisa realizar o faturamento de clientes tem um JOB que levanta os IDs dos clientes e distribui para outras máquinas processarem o faturamento daqueles clientes. O ponto ruim é que essa "máquina gerente" acumula muita responsabilidade e pode ser um gargalo. Esse trecho de código que levanta os dados só pode rodar em uma única instancia para não duplicar o processamento.
  • Podemos usar soluções de filas e mensagerias. Soluções como Kafka ou outras.
  • Podemos usar o Lock do banco de dados de forma otimizada. Essa é a solução apresentada pelo Rafael. Para isso, acrescenta-se o ao lock do banco a possibilidade de pular os dados locados e pegar o próximo. Vamos ver o que precisa ser adicionado em termos de anotação Java:



        E no SQL diretamente ficaria assim:


        Mais uma vez fica a dica de garantir que o método esteja otimizado, que dentro da tranzação esteja apenas o que precisa estar lá, para que o lock dure apenas o necessário.

        Essa solução é muito interessante porque permite que várias instâncias executem o mesmo código garantindo que os dados serão respeitados por todas elas como mostra a imagem abaixo:



        Aqui temos todo o código de exemplo trazido por ele na aula:



        Aconselho assistir a aula, tem mais coisas interesantes lá!


Link: aula do Rafael Ponte sobre Distributed Scheduling





quinta-feira, 9 de outubro de 2025

Versões do OpenJDK para download

         As vezes pra quem está começando pode ser complicado achar onde baixar as versões do Java, então deixei aqui o link que vai direto para a página de downloads do OpenJDK. Basta clicar AQUI.

        Infelizmente, com as diretrizes da Oracle, as versões mais antigas do Java vão ficando indisponíveis mesmo nessa página, como é o caso da versão 8 que até a data de hoje (09/10/2025) ainda é muito usada porém não aparece mais para download. Com isso, é preciso uma busca mais cuidadosa e demorada caso precise de uma versão tão antiga. Talvez ache apenas de outros distribuidores. Sempre pesquise bem antes de confiar!

sábado, 24 de agosto de 2024

Kafka - Rodando no WSL 2 (Ubuntu) e usando (Producer e Consumer) a partir do Windows

        Lá estava eu estudando Kafka e me deparei com a seguinte situação: O Kafka instalado no Ubuntu através do WSL 2, rodando legal e eu criando produtores e consumidores a partir do Java usando o Intellij no Windows. Daí o que ocorre é: o Kafka sobe normal, mas o projeto Java não consegue encontrar o serviço, mesmo colocando no projeto o IP do WSL.

      Então descobri que para fazer o projeto Java se comunicar com o Kafka no WSL 2 era preciso configurar a propriedade advertised.listeners no arquivo server.properties no diretório config do Kafka: 


advertised.listeners=PLAINTEXT://ip_do_wsl:9092


        Dessa forma a aplicação mesmo em outra rede consegue encontrar o Kafka e criar e ler os tópicos, produzindo e consumindo suas menagens. A propriedade advertised.listeners no Apache Kafka desempenha um papel crucial na comunicação entre os clientes (produtores e consumidores) e os brokers do Kafka, especialmente em cenários de redes distribuídas ou ambientes com várias interfaces de rede.

        Ela especifica os endereços IP ou nomes de host e portas que os brokers Kafka devem anunciar para os clientes (produtores, consumidores, etc.). Esses são os endereços que os clientes usarão para se conectar ao broker após receberem a metadata inicial do Kafka.

        Uma questão é: o IP do WSL pode mudar toda vez que for iniciado. Ficar mudando isso tanto no arquivo de configuração do Kafka quanto na aplicação (ou aplicações como é o caso microsseviços) é chato. Então pra mudar na aplicação (no meu caso Java) muda muito de caso pra caso, mas a ideia inicial seria usar variável de ambiente ou arquivo de propriedades tirando do código a dependência. 

     Para o Kafka recorri ao GPT que gerou o script bash a seguir que troca o IP no arquivo config/server.properties:


#!/bin/bash

# Passo 1: Obter o IP do WSL 2

WSL_IP=$(hostname -I | awk '{print $1}')


# Passo 2: Caminho para o arquivo server.properties do Kafka

KAFKA_CONFIG_PATH="/caminho/para/seu/kafka/config/server.properties"


# Passo 3: Atualizar o advertised.listeners com o IP atual do WSL 2

# Verifique se o arquivo existe

if [ -f "$KAFKA_CONFIG_PATH" ]; then

    # Usar sed para substituir o advertised.listeners existente ou adicionar se não existir

    if grep -q "^advertised.listeners=" "$KAFKA_CONFIG_PATH"; then

        # Substitui a linha existente

        sed -i "s/^advertised.listeners=.*/advertised.listeners=PLAINTEXT:\/\/$WSL_IP:9092/" "$KAFKA_CONFIG_PATH"

    else

        # Adiciona a linha se não existir

        echo "advertised.listeners=PLAINTEXT://$WSL_IP:9092" >> "$KAFKA_CONFIG_PATH"

    fi

    echo "advertised.listeners atualizado com o IP: $WSL_IP"

else

    echo "Erro: Arquivo server.properties não encontrado em $KAFKA_CONFIG_PATH"

    exit 1

fi


# Passo 4: Iniciar ou reiniciar o Kafka para aplicar as mudanças

# Ajuste o comando abaixo para iniciar ou reiniciar seu Kafka

# Exemplo para iniciar Kafka:

# /caminho/para/seu/kafka/bin/kafka-server-start.sh -daemon $KAFKA_CONFIG_PATH

echo "Script concluído. Certifique-se de reiniciar o Kafka para aplicar as mudanças."


       Basta salvar o script como atualiza_kafka.sh na pasta do Kafka, dar permissão de execução com chmod +x atualiza_kafka.sh e executar antes de subir o Kafka.

        Ainda não executei o Kafka pelo Docker subindo também os produtores e consumidores em uma mesma rede com Docker Compose então não sei se muda muita coisa, mas como seria a mesma rede, acredito que essa configuração de advertised.listeners não seja necessária, de qualquer forma esse artigo pode ser útil pra mim mesmo no futuro e outros que estão aprendendo a ferramenta.







domingo, 4 de agosto de 2024

Metodologia Twelve-Factor

         A metodologia Twelve-Factor App é um conjunto de 12 regras para manter uma aplicação de forma eficaz, sendo elas:


  1. Codebase (Código-fonte único): Uma aplicação deve ser armazenada em um único repositório de código, com múltiplas implantações derivadas do mesmo código-base.

  2. Dependencies (Dependências explícitas): Todas as dependências da aplicação, incluindo bibliotecas e ferramentas de sistema, devem ser declaradas explicitamente e gerenciadas de forma isolada.

  3. Config (Configurações): As configurações da aplicação devem ser armazenadas em variáveis de ambiente, não no código-fonte, para permitir a configuração flexível em diferentes ambientes.

  4. Backing Services (Serviços de Back-end): Os serviços de back-end, como bancos de dados e filas, devem ser tratados como recursos externos, acessíveis por meio de interfaces padrão.

  5. Build, Release, Run (Construir, entregar, executar): O processo de build, entrega e execução da aplicação deve ser separado em etapas distintas, com cada etapa tendo suas próprias responsabilidades e garantindo a consistência entre ambientes.

  6. Processes (Processos): As aplicações devem ser executadas como processos independentes, leves e sem estado (Stateless), para facilitar a escalabilidade e a resiliência.

  7. Port Binding (Ligação de porta): As aplicações devem ser autocontidas e expor serviços por meio de portas, para que possam ser facilmente conectados a outras aplicações e serviços.

  8. Concurrency (Concorrência): As aplicações devem escalar horizontalmente, adicionando instâncias concorrentes para lidar com cargas de trabalho aumentadas.

  9. Disposability (Descartabilidade): As aplicações devem ser fáceis de iniciar e parar rapidamente, sem impacto para outras partes do sistema, para facilitar o deploy e a atualização contínua.

  10. Dev/Prod Parity (Paridade dev/prod): Os ambientes de desenvolvimento, testes e produção devem ser o mais semelhantes possível, para minimizar diferenças e evitar problemas de compatibilidade.

  11. Logs (Registros): As aplicações devem produzir logs estruturados e acessíveis por meio de interfaces padronizadas, para facilitar a depuração e o monitoramento.

  12. Admin Processes (Processos de administração): As tarefas administrativas, como migrações de banco de dados e limpeza de caches, devem ser executadas como processos únicos e rastreáveis.

Balanceamento com NGINX

         Além de expor conteúdo estático e proxy reverso, os servidores web também fazem balanceamento entre nós da aplicação. Nesse artigo vou mostrar o NGINX fazendo o balanceamento entre 2 nós.

         Para fazer o balanceamento, no arquivo nginx.conf que fica dentro de /etc/nginx/conf.d declaramos os servidores (com ip e porta) na seção upstream e depois usamos no proxy pass. Cada upstream deve ter um nome para poder ser referenciado no server, nesse caso o nome foi meu_upstream:


upstream meu_upstream{

        # ip_hash habilita sticky sessions com base no endereço IP do cliente, para garantir a seção        

        ip_hash;

server ip_server_01:8080;

server ip_server_02:8080;

}


server {

    listen 80;

    

    location / {

        proxy_pass http://meu_upstream;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header X-Forwarded-Proto $scheme;

    }

}


        Pronto, dessa forma temos um balanceamento configurado.

Redis com Spring Boot

         O Redis é um banco de dados em memória para criar caches na aplicação, tornando o acesso a dado mais rápidos.

      Claro que não é simplesmente fazer cache de toda a aplicação, por isso é preciso entender como funciona o sistema. O cache é indicado para tabelas que tem bastante acesso e pouca alteração, diminuindo dessa forma o acesso desnecessário ao banco e ajudando na saúde da aplicação como um todo, pois desonerando o acesso ao banco com coisas que mudam pouco deixamos ele livre para responder a requisições mais importantes.

        Pra começarmos, precisamos adicionar a dependência no projeto:


<dependency>

<groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-cache</artifactId>

</dependency>


    Uma vez adicionada a dependência, é preciso adicionar o endereço e a porta no arquivo application.properties:


spring.data.redis.host=127.0.0.1

spring.data.redis.port=6379


        É preciso também habilitar o Cache na aplicação, assim como é habilitado as requisições assíncronas, com o uso de anotação na classe principal:


@EnableCaching


        Agora o Redis está configurado para ser usado, o que falta é usar o Cache no projeto. Os métodos que desejamos que tenham seu resultado guardado no cache (geralmente estão nas classes de negócio - também chamadas de services - ou no DAO). Basta anotá-los com @Cacheable e dar um nome para esses dados na tabela de cache:


@Cacheable(value = "nome_da_tb_de_cache")


        E para atualizar os dados, fazendo o sistema ir buscar no banco? Podemos anotar os métodos de inserção, update e exclusão com @CacheEvict passando o nome da tabela de cache e se queremos limpar todos os registros:


@CacheEvict(value = "nome_da_tb_de_cache", allEntries = true)


     Isso vai limpar os dados. Também podemos definir um tempo de vida (TTL) para o cache no application.properties, o que dá a ele um tempo padrão de vida (o default é ficar pra sempre).


# TTL em milissegundos (10 minutos)

spring.cache.redis.time-to-live=600000  


        Existem outras formas que podem ser pesquisadas caso haja necessidade, como o tamanho do cache.









terça-feira, 30 de julho de 2024

Pool de conexões no Spring Boot

         Toda aplicação precisa de um pool de conexões para gerenciar o acesso ao banco de dados de forma eficiente. No Spring Boot é carregado um pool automaticamente, mas devemos configurar para o ambiente de produção de acordo com a necessidade.

        

        A configuração varia de acordo com cada aplicação, então é preciso testar e observar o comportamento da aplicação pra saber se mais conexões disponíveis são necessárias.


        Segue um exemplo de configurações de pool no Spring Boot:


spring.datasource.hikari.minimum-idle=25

spring.datasource.hikari.maximum-pool-size=50

spring.datasource.hikari.connectionTimeout=15000  

spring.datasource.hikari.idleTimeout=600000

spring.datasource.hikari.maxLifetime=1800000 



HTTPS no NGINX

        Para habilitar o HTTPS em um servidor, é necessário adquirir um certificado digital SSL/TLS de uma Autoridade Certificadora (CA) confiável. É possível obter um certificado digital de maneira gratuita, utilizando o serviço Let's Encrypt.

        Para usar o HTTPS no Nginx e configurar o certificado digital é preciso copiar o certificado e a cheve para dentro do servidor nos caminhos mostrados abaixo:

        Certificado ssl:                     /etc/nginx/certificates/certificado.crt;

        Chave ssl do certificado:     /etc/nginx/certificates/chave.key;

        E então mudar o arquivo nginx.conf de: 

server {
    listen 80;
    server_name meu_dominio.com.br;
    
    location / {
        proxy_pass http://ip_servidor:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

        Para:

server {
    listen 80;
    server_name meu_dominio.com.br;
location / { return 301 https://$host$request_uri; } } server { listen 443 ssl; server_name meu_dominio.com.br;
ssl_certificate /etc/nginx/certificates/certificado.crt; ssl_certificate_key /etc/nginx/certificates/chave.key; location / { proxy_pass http://ip_servidor:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }