ReactiveLock – Um case pronto para produção da Rinha de Backend 2025

Michel Oliveira
9 min read
ReactiveLock – Um case pronto para produção da Rinha de Backend 2025

Introdução

Participei da Rinha de Backend 2025, um desafio de alta performance para sistemas distribuídos, enviando diversas versões para avaliação de desempenho e consistência, incluindo:

  • gRPC puro: comunicação eficiente entre instâncias sem backend adicional;
  • Redis com gRPC: coordenação distribuída com Redis como backend e replicação de estado via gRPC;
  • Redis apenas: Redis como único backend de sincronização e enfileiramento de tarefas;
  • PostgreSQL + Redis: combinando bulk inserts otimizados no PostgreSQL com Redis para enfileiramento e coordenação com a biblioteca ReactiveLock;
  • HAProxy e NGINX: implementações para avaliar o impacto de proxies reversos na distribuição de tráfego e estabilidade do sistema.

O ponto central da minha participação foi o ReactiveLock, biblioteca que publiquei em código aberto para lidar com a sincronização reativa de locks distribuídos. Ao longo da minha experiência, precisei implementar diversas vezes estruturas semelhantes, mas existiam poucas opções de código aberto disponíveis. Por isso, tomei a iniciativa de criar uma solução própria, permitindo que múltiplas instâncias compartilhem estado de forma eficiente, aproveitando Redis e gRPC como backends de coordenação.

ReactiveLock é uma biblioteca .NET (8/9+) desenvolvida para coordenação reativa de locks distribuídos. Ela permite que múltiplas instâncias de aplicação monitorem e reajam ao estado ocupado/ocioso dos recursos de forma assíncrona e orientada a eventos. O principal objetivo ao criar ReactiveLock foi mitigar os trade-offs do Teorema CAP, garantindo que sistemas distribuídos permaneçam responsivos mesmo em situações de falhas temporárias, enquanto asseguram convergência eventual do estado dos locks.

A biblioteca não é apenas uma solução conceitual: já está sendo usada em módulos que executam em produção com alto throughput, comprovando sua eficiência em cenários reais e críticos, onde centenas de operações de lock ocorrem por segundo.

O que é o ReactiveLock

ReactiveLock é um sistema de coordenação de locks distribuídos e reativo que oferece:

  • Incremento e decremento de contadores: Representando tarefas ou requisições ativas.
  • Consulta de estado em tempo real: Permite verificar se há atividades em andamento.
  • Espera assíncrona: Tarefas podem aguardar o desbloqueio de forma eficiente, liberando recursos assim que possível.

Por padrão, o estado compartilhado é mantido em Redis ou gRPC com outro backend implementado, permitindo sincronização entre instâncias e garantindo que todas tenham consciência do estado do sistema.

Comparação com Outras Bibliotecas de Lock

Enquanto muitas bibliotecas resolvem problemas similares de locks distribuídos, elas diferem na forma como gerenciam o estado dos locks.

ReactiveLock é fortemente orientado a eventos, minimizando overhead ao gerenciar o estado do lock em memória antes de propagá-lo entre instâncias distribuídas. A maioria das outras bibliotecas prioriza consistência forte, realizando chamadas ativas ao backend (por exemplo, Redis) a cada operação de lock. Essa abordagem garante locks precisos, mas pode afetar significativamente a performance, especialmente em workloads de alta intensidade, onde locks podem ser adquiridos milhares de vezes por segundo.

ReactiveLock foi projetado para equilibrar reatividade com coordenação distribuída, tornando-se ideal para cenários onde alto throughput é crítico.

Benefícios da Biblioteca

O ReactiveLock traz benefícios claros para aplicações distribuídas:

  • Alta performance: Prioriza o estado do lock em memória, reduzindo chamadas ao backend e latência.
  • Reatividade: Notifica automaticamente waiters assíncronos quando o estado muda.
  • Convergência eventual confiável: Estados de lock convergem mesmo após falhas temporárias.
  • Flexibilidade: Pode operar em modo local (single-instance) ou distribuído (Redis ou gRPC com outro backend).
  • Resiliência: Suporta retries, políticas de fallback e tempos de expiração para mitigar falhas e prover contenção.
  • Testada em condições extremas: A suíte de testes de integração utiliza os scripts da Rinha de Backend 2025, simulando mais de 1 milhão de requisições, validando alta concorrência e coordenação correta entre instâncias.

Componentes Principais

  • TrackerController: Controla o lock via contagem de referências. IncrementAsync() aumenta o contador e marca o lock como bloqueado; DecrementAsync() reduz o contador e libera o lock quando apropriado.
  • TrackerState: Mantém o estado atual do lock e notifica tarefas que aguardam desbloqueio.
  • TrackerStore: Persiste e sincroniza o estado local ou distribuído, propagando mudanças para outras instâncias.
  • Async Waiters: Permitem que tarefas aguardem o desbloqueio sem bloquear threads, reagindo automaticamente quando o estado muda.

Arquitetura Base

flowchart TD subgraph Instância["Instância da Aplicação"] Controlador["TrackerController<br/>(Increment / Decrement)"] Estado["TrackerState<br/>(Bloqueado / Desbloqueado)"] Esperas["Async Waiters / Handlers"] end subgraph Lojas["TrackerStore"] Local["InMemory Store<br/>(Modo Local)"] Dist["Distributed Store<br/>(Redis / gRPC)"] end Backend["Backend Distribuído<br/>(Redis / Servidor gRPC)"] Controlador -->|atualiza| Lojas Lojas -->|propaga| Estado Estado -->|notifica| Esperas Esperas -->|reage| Estado Dist <--> Backend

Fluxo Básico de Lock

sequenceDiagram participant App1 as Instância 1 participant Backend as Backend Distribuído participant App2 as Instância 2 App1->>Backend: IncrementAsync() Backend-->>App2: Notifica estado *busy* note right of App2: Instância 2 atualiza o estado para bloqueado App1->>App1: Executa tarefa crítica App1->>Backend: DecrementAsync() Backend-->>App2: Notifica estado *idle* note right of App2: Instância 2 libera as tarefas que estão aguardando

Detalhe do Fluxo Básico

sequenceDiagram participant Inst1 as Instância 1 participant Inst2 as Instância 2 participant Backend as Redis/gRPC %% Instância 1 altera o lock Inst1->>Backend: Declara a instância 1 como lock ocupado Backend-->>Inst1: Recebe a notificação que alguma instância mudou o estado dos locks Backend-->>Inst2: Notifica o lock ocupado da instância 1 %% Instâncias resolvem o lock Inst1->>Inst1: Resolve localmente o lock até o próximo evento Inst2->>Inst2: Resolve localmente o lock até o próximo evento

Nesses diagramas de sequência, a Instância 1 declara um lock de uma tarefa crítica e envia essa informação para o backend (Redis ou gRPC). O backend confirma a operação de lock para a própria Instância 1 e notifica a Instância 2 sobre o estado ocupado do lock. Em seguida, ambas as instâncias processam a informação localmente: cada uma resolve o lock em sua própria memória, garantindo que o estado compartilhado esteja consistente e permitindo que tarefas dependentes do lock sejam liberadas de forma reativa em todas as instâncias até que outro evento de lock ocorra.

Além disso, o lock é resolvido considerando o estado de todas as instâncias. Ou seja, quando notificadas sobre mudança de locks, cada instância verifica se outras instâncias ainda possuem o lock ativo; enquanto uma ou mais instâncias estiverem com o lock ocupado, o lock local permanece bloqueado. Somente quando todas as instâncias liberam o lock, o estado local é atualizado para desbloqueado, garantindo consistência global e evitando que tarefas críticas sejam executadas antes da liberação completa do recurso.

Uso na Rinha de Backend 2025 e Case de Produção

Durante o desafio Rinha de Backend 2025, ReactiveLock foi usado para:

  • Garantir consistência entre múltiplas instâncias de API, tanto no fluxo de processamento quanto na leitura de sumários.
  • Permitir que dados fossem processados em lotes, implementando agregação de pagamentos antes da execução de forma segura. O ReactiveLock garantiu a consistência durante essas agregações, evitando conflitos e perdas de dados. Esse comportamento foi aplicado tanto na versão com gRPC, quanto nas implementações com Redis e Postgres, sempre com base no controle de locks fornecido pelo ReactiveLock.
  • Sincronizar estados via gRPC, garantindo que as instâncias distribuídas tenham visibilidade do lock e das tarefas em memória.

Além disso, o ReactiveLock é utilizado no SaaS Propostize para garantir a consistência do cálculo de propostas. Diferente de depender apenas do Postgres, os locks são resolvidos reativamente em memória antes da consolidação dos dados no banco, formando uma camada de bloqueio que reduz significativamente o consumo de recursos. O Postgres ainda realiza a transação como antes, porém com menor tempo e custo graças a essa camada reativa. Em momentos de pico, mais de 1000 locks por segundo são processados.

O uso durante a Rinha e em produção demonstra que ReactiveLock consegue lidar com cenários de alto throughput sem comprometer a consistência, permitindo processamento seguro e eficiente.

  • Esse é um diagrama simplificado apenas do lock que persiste as conexões HTTP no contexto da Rinha de Backend:
sequenceDiagram participant HTTP as Processos HTTP (requisições) participant Inst1 as Instância 1 participant Inst2 as Instância 2 participant Backend as Redis/gRPC participant Sum as API de Sumário %% Alteração de lock pelo processo HTTP HTTP->>Inst1: Estado bloqueado (lock ativo) Inst1->>Backend: Propaga lock Backend-->>Inst1: Notifica sobre o lock Backend-->>Inst2: Notifica sobre o lock Inst1->>Inst1: Processa a requisição HTTP->>Inst1: Estado desbloqueado (lock liberado) Inst1->>Backend: Propaga lock liberado Backend-->>Inst1: Notifica sobre o lock liberado Backend-->>Inst2: Notifica sobre o lock liberado %% API de sumário externa aguarda enquanto há locks ativos Sum->>Backend: Consulta locks ativos alt Locks ativos Sum->>Sum: Aguarda else Nenhum lock Sum->>Sum: Retorna sumário end

Em uma representação completa, para que o sumário possa ser executado, existe uma sincronização não apenas entre as requisições pendentes, mas também sobre os processos pendentes, como bulk inserts. Isso garante consistência e evita que dados incompletos sejam retornados ao consumir a API de sumário.

Mitigando o Teorema CAP

ReactiveLock equilibra disponibilidade e consistência. Em cenários de partições de rede, a biblioteca permite que a aplicação continue respondendo (alta disponibilidade) utilizando estados locais em memória, enquanto a sincronização garante convergência do lock entre instâncias. Isso é particularmente importante em sistemas de produção de alto throughput, permitindo que falhas temporárias não interrompam o fluxo de processamento.

Para entender mais sobre o CAP, leia:

Como Integrar o ReactiveLock

Exemplo de uso simples:

// Cria uma nova instância do estado do tracker
var estado = new ReactiveLockTrackerState();

// Define o estado local como bloqueado (simula um lock sendo mantido)
await estado.SetLocalStateBlockedAsync();

// Inicia 3 tarefas que irão esperar até que o estado seja desbloqueado
var tarefas = Enumerable.Range(1, 3).Select(i =>
    Task.Run(async () => {
        Console.WriteLine($"[Tarefa {i}] Aguardando...");

        // Cada tarefa ficará aqui esperando até que o estado seja desbloqueado
        await estado.WaitIfBlockedAsync();

        // Uma vez desbloqueado, esta mensagem será exibida
        Console.WriteLine($"[Tarefa {i}] Continuou.");
    })
).ToArray();

// Simula um atraso antes de desbloquear o estado
await Task.Delay(1000);

// Desbloqueia o estado (libera todas as tarefas que estão esperando)
await estado.SetLocalStateUnblockedAsync();

// Aguarda todas as tarefas completarem
await Task.WhenAll(tarefas);

// Indica que o processo terminou
Console.WriteLine("Concluído.");

Exemplo de uso com contagem de processos:

var state = new ReactiveLockTrackerState();
var store = new InMemoryReactiveLockTrackerStore(state);
var controller = new ReactiveLockTrackerController(store);

// Bloqueia inicialmente o estado (ex.: lock adquirido)
await controller.IncrementAsync(); // Bloqueado

var tasks = Enumerable.Range(1, 3).Select(i =>
Task.Run(async () =>
{
    Console.WriteLine($"[Tarefa {i}] Aguardando...");
    await state.WaitIfBlockedAsync(); // Aguarda enquanto estiver bloqueado
    Console.WriteLine($"[Tarefa {i}] Continuou.");
})
).ToArray();

// Simula um atraso antes de liberar o lock
await Task.Delay(1000);

// Libera o lock (desbloqueado)
await controller.DecrementAsync(); // Desbloqueado

await Task.WhenAll(tasks);

Console.WriteLine("Concluído.");

Mais exemplos em:

Conclusão

ReactiveLock é um case consolidado de alto desempenho, nascido na Rinha de Backend 2025 e validado em produção. Ele oferece coordenação eficiente de locks distribuídos, alta reatividade e resiliência em cenários de concorrência. Com integração fácil via injeção de dependências, suporte a Redis, gRPC com múltiplos backend possíveis, e testes robustos que simulam mais de um milhão de requisições, é uma ferramenta pronta para produção, capaz de mitigar trade-offs do CAP e otimizar sistemas distribuídos em .NET.

Referências

Conecte-se para transformar sua tecnologia!

Saiba mais e entre em contato: