Estratégias de Bloqueio Distribuído com Redis em Arquiteturas Escaláveis

O Desafio da Consistência em Sistemas Distribuídos

Em cenários de alta concorrência, como sistemas de vendas flash ou "seckill", garantir a integridade dos dados é fundamental. Quando múltiplas instâncias de um serviço tentam manipular o mesmo recurso (por exemplo, o estoque de um produto), o uso de travas locais do Java torna-se insuficiente.

Limitações de Travas em Nível de JVM

Inicialmente, poderíamos tentar resolver o problema de concorência usando mecanismos internos da linguagem, como synchronized ou ReentrantLock. No entanto, essas ferramentas operam apenas dentro de uma única instância da Java Virtual Machine (JVM).

@GetMapping("/venda/local")
public String processarVendaLocal() {
    synchronized (this) {
        // Recupera o estoque atual do cache
        Integer estoqueAtual = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
        
        if (estoqueAtual != null && estoqueAtual > 0) {
            int novoEstoque = estoqueAtual - 1;
            redisTemplate.opsForValue().set("estoque:item:101", novoEstoque);
            return "Venda concluída! Restam: " + novoEstoque;
        }
        return "Produto esgotado.";
    }
}

Em um ambiente distribuído com múltiplas instâncias atrás de um Load Balancer, cada servidor terá seu próprio objeto de trava, permitindo que diferentes processos reduzam o estoque simultaneamente, causando a venda de itens inexistentes (overselling).

Implementação com SETNX (Set if Not Exists)

Para resolver o isolamento entre diferentes nós, utilizmaos o Redis como um gerenciador de bloqueios centralizado. O comando SETNX permite definir uma chave apenas se ela não existir, funcionando como um semáforo atômico.

@GetMapping("/venda/distribuida-v1")
public String vendaComSetNX() {
    String chaveBloqueio = "lock:produto:101";
    String idRequisicao = UUID.randomUUID().toString();
    
    try {
        // Tenta adquirir o bloqueio com tempo de expiração (TTL) para evitar deadlocks
        Boolean sucesso = redisTemplate.opsForValue()
                .setIfAbsent(chaveBloqueio, idRequisicao, Duration.ofSeconds(15));

        if (Boolean.FALSE.equals(sucesso)) {
            return "Servidor ocupado, tente novamente.";
        }

        // Lógica de negócio
        Integer qtd = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
        if (qtd > 0) {
            redisTemplate.opsForValue().set("estoque:item:101", qtd - 1);
            return "Sucesso!";
        }
        return "Esgotado.";
        
    } finally {
        // Liberação segura: verifica se o token ainda pertence a esta requisição
        if (idRequisicao.equals(redisTemplate.opsForValue().get(chaveBloqueio))) {
            redisTemplate.delete(chaveBloqueio);
        }
    }
}

Embora funcional, esta abordagem possui vulnerabilidades: o tempo de execução da lógica pode exceder o TTL do bloqueio, ou o processo pode falhar entre a verificação e a exclusão da chave.

Solução Robusta com Redisson

A biblioteca Redisson abstrai a complexidade do gerenciamento de bloqueios. Ela oferece travas reentrantes e implementa o mecanismo de Watchdog, que renova automaticamente o tempo de vida do bloqueio anquanto a thread detentora ainda estiver processando a tarefa.

@GetMapping("/venda/redisson")
public String vendaRedisson() {
    RLock trava = redissonClient.getLock("trava:produto:101");
    
    try {
        // Aguarda até 5 segundos para obter a trava, com expiração de 10 segundos
        if (trava.tryLock(5, 10, TimeUnit.SECONDS)) {
            Integer saldo = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
            
            if (saldo > 0) {
                int saldoFinal = saldo - 1;
                redisTemplate.opsForValue().set("estoque:item:101", saldoFinal);
                return "Reserva confirmada. Estoque: " + saldoFinal;
            }
            return "Indisponível.";
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (trava.isLocked() && trava.isHeldByCurrentThread()) {
            trava.unlock();
        }
    }
    return "Falha ao processar.";
}

Vantagens do Redisson:

  • Atomicidade: Utiliza scripts Lua para garantir que as operações de verificação e criação sejam executadas como uma única unidade no Redis.
  • Reentrância: Permite que a mesma thread adquira o bloqueio múltiplas vezes sem se auto-bloquear.
  • Watchdog: Evita que o bloqueio expire prematuramente se a lógica de negócio demorar mais que o previsto.

Considerações sobre Redlock

Em ambientes de produção com clusters Redis, o algoritmo Redlock é recomendado. Enquanto o Redisson padrão foca em um único nó master, o Redlock exige que o bloqueio seja adquirido na maioria dos nós do cluster (quorum). Isso previne problemas de consistência em situações onde o nó master falha antes de sincronizar a chave de bloqueio com seus réplicas.

Tags: Redis Redisson DistributedSystems java Concurrency

Publicado em 7-16 01:49