Implementação de Travas Distribuídas com Redis para Garantir Unicidade em Ambientes de Cluster

Considere uma classe Pessoa onde o campo nome deve ser único no sistema.

public class Pessoa {
    private int id;
    private String nome; // Deve ser garantido único
    private int idade;

    // Getters e setters omitidos
}

Uma abordagem comum é utilizar restrições de unicidade no banco de dados. Entretanto, ao adicionar um campo deletado (indicando remoção lógica), a restrição de unicidade torna-se inviável para cenários de reutilização de nomes.

public class Pessoa {
    private int id;
    private String nome; // Deve ser garantido único
    private int idade;
    private int deletado; // 1: removido, 0: ativo

    // Getters e setters omitidos
}

Nesse caso, a garantia de unicidade precisa ser implementada no código da aplicação. Em um ambiente de aplicação único (standalone), o uso de synchronized pode resolver o problema.

public void cadastrarPessoa(Pessoa pessoa) {
    // 1. Busca pessoa existente pelo nome
    Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
    if (existente == null) {
        // 2. Sincroniza pelo nome (usando pool de strings)
        synchronized (pessoa.getNome().intern()) {
            // 3. Verifica novamente após obter o lock
            existente = buscarPessoaPorNome(pessoa.getNome());
            if (existente == null) {
                // 4. Persiste no banco de dados
                String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
                jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
            } else {
                throw new IllegalArgumentException("Nome já cadastrado.");
            }
        }
    } else {
        throw new IllegalArgumentException("Nome já cadastrado.");
    }
}

Porém, em um ambiente de cluster com múltiplas instâncias da aplicação, o synchronized não oferece proteção, pois cada instância mantém seu próprio monitor. Consequentemente, duas requisições simultâneas para o mesmo nome podem ser processadas por diferentes servidores, resultando em duplicidade.

2. Solução com Travas Distribuídas via Redis

Para resolver a concorrência em cluster, utiliza-se uma trava distribuída com Redis. O comando SETNX (SET if Not eXists) garante que apenas um cliente consiga definir uma chave específica, desde que ela não exista.

Fluxo típico:

  1. Verificar se o nome já está cadastrado no banco de dados.
  2. Se não existir, solicitar uma trava no Redis associada àquele nome.
  3. Se a trava for obtida, proceder com a inserção e, após a operação, liberar a trava.
  4. Se a trava não for obtida, informar que o recurso está ocupado.

Implementação usando Spring Data Redis:

public void cadastrarPessoa(Pessoa pessoa) {
    // 1. Verifica existência do nome
    Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
    if (existente == null) {
        // 2. Tenta adquirir a trava distribuída (chave única por nome, expira em 60 segundos)
        String chaveTrava = "trava_nome_" + pessoa.getNome();
        Boolean adquiriuTrava = stringRedisTemplate.opsForValue().setIfAbsent(chaveTrava, "bloqueado", 60, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(adquiriuTrava)) {
            try {
                // 3. Simulação de operação demorada
                Thread.sleep(5000);
                // 4. Persistência dos dados
                String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
                jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
            } finally {
                // 5. Libera a trava no bloco finally para garantir a execução
                stringRedisTemplate.delete(chaveTrava);
            }
        } else {
            throw new IllegalArgumentException("Serviço ocupado. Tente novamente mais tarde.");
        }
    } else {
        throw new IllegalArgumentException("Nome já cadastrado.");
    }
}

3. Aperfeiçoamento: Renovação Automática da Trava

Cenários de execução prolongada podem levar à expiração da trava antes da conclusão da transação, permitindo que outra thread adquira a trava e cause inconsistência. Para mitigar isso, implementa-se um mecanismo de renovação automática através de uma thread daemon.

public void cadastrarPessoa(Pessoa pessoa) {
    // Verificação e tentativa de aquisição da trava
    if (existente == null) {
        String chaveTrava = "trava_nome_" + pessoa.getNome();
        Boolean adquiriuTrava = stringRedisTemplate.opsForValue().setIfAbsent(chaveTrava, "bloqueado", 30, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(adquiriuTrava)) {
            // Inicia thread daemon para monitorar e renovar a trava
            Thread daemonRenovacao = new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(10000); // Verifica a cada 10 segundos
                        Long tempoExpiracao = stringRedisTemplate.getExpire(chaveTrava);
                        if (tempoExpiracao == -2) { // Chave não existe
                            break;
                        }
                        if (tempoExpiracao < 15) { // Se expirar em menos de 15 segundos
                            stringRedisTemplate.expire(chaveTrava, 30, TimeUnit.SECONDS); // Renova
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            });
            daemonRenovacao.setDaemon(true);
            daemonRenovacao.start();

            try {
                // Lógica de negócio principal
                String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
                jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
            } finally {
                stringRedisTemplate.delete(chaveTrava); // Libera a trava
            }
        } else {
            throw new IllegalArgumentException("Serviço ocupado.");
        }
    } else {
        throw new IllegalArgumentException("Nome já cadastrado.");
    }
}

Considerações:

  • Thread Daemon: Utilizada para que a renovação ocorra em segundo plano e encerre automaticamente quando a thread principal finalizar.
  • Tempo de Expiração: Essencial para evitar que travas fiquem permanentemente no Redis em caso de falhas (ex.: queda de energia).

4. Uso do Redisson para Simplificação

A implementação manual de travas distribuídas é complexa e suscetível a erros. O Redisson, um cliente Redis avançado, fornece componentes robustos como o watchdog para renovação automática de travas.

4.1 Configuração com Spring Boot

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>
@Bean
public RedissonClient clienteRedis() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://localhost:6379");
    // Configura o tempo de monitoramento do watchdog (20 segundos)
    config.setLockWatchdogTimeout(20000);
    return Redisson.create(config);
}

4.2 Exemplo de Código com Redisson

@Autowired
private RedissonClient clienteRedis;

public void cadastrarPessoa(Pessoa pessoa) throws InterruptedException {
    Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
    if (existente == null) {
        RLock trava = clienteRedis.getLock("trava_pessoa_" + pessoa.getNome());
        boolean adquiriu = trava.tryLock(5, 30, TimeUnit.SECONDS); // Espera 5s, expira em 30s
        if (adquiriu) {
            try {
                // Lógica de negócio (pode ser demorada)
                String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
                jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
            } finally {
                trava.unlock(); // Libera a trava
            }
        } else {
            throw new IllegalArgumentException("Não foi possível obter a trava.");
        }
    } else {
        throw new IllegalArgumentException("Nome já cadastrado.");
    }
}

Observação: Para que o watchdog do Redisson funcione, a trava não deve ser adquirida com um tempo de expiração explícito no método tryLock(). Utilize versões sem parâmetros de tempo ou com apenas tempo de espera.

4.3 Travas Multi-Instância com RedissonMultiLock

Em ambientes Redis com replicação (mestre-escravo), há o risco de perda de travas durante uma falha no nó mestre antes da sincronização. O RedissonMultiLock permite aplicar a trava em múltiplos nós simultaneamente (trava em cluster), aumentando a disponibilidade.

@Bean
public RedissonClient clienteRedis2() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://segundo-servidor:6379");
    config.setLockWatchdogTimeout(20000);
    return Redisson.create(config);
}

@Bean
public RedissonClient clienteRedis3() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://terceiro-servidor:6379");
    config.setLockWatchdogTimeout(20000);
    return Redisson.create(config);
}

// Exemplo de uso em serviço
@Autowired private RedissonClient clienteRedis;
@Autowired private RedissonClient clienteRedis2;
@Autowired private RedissonClient clienteRedis3;

public void operacaoCritica() throws InterruptedException {
    RLock trava1 = clienteRedis.getLock("chave_operacao");
    RLock trava2 = clienteRedis2.getLock("chave_operacao");
    RLock trava3 = clienteRedis3.getLock("chave_operacao");

    RedissonMultiLock travaMultipla = new RedissonMultiLock(trava1, trava2, trava3);
    boolean adquiriu = travaMultipla.tryLock(10, 60, TimeUnit.SECONDS);
    if (adquiriu) {
        try {
            // Execução da operação protegida
        } finally {
            travaMultipla.unlock();
        }
    }
}

5. Considerações Finais sobre Limitações

Travas distribuídas, mesmo com mecanismos avançados, estão sujeitas a falhas em cenários extremos:

  • Bloqueio de threads ou do próprio watchdog durante a renovação.
  • Falhas simultâneas em múltiplos nós Redis.
  • Problemas de rede que impeçam a comunicação entre a aplicação e o Redis.

A implementação visa minimizar riscos, mas não elimina completamente a possibilidade de inconsistências. É crucial monitorar a infraestrutura e ter planos de recuperação.

Tags: Redis Travas Distribuídas java Spring Boot Redisson

Publicado em 6-12 04:38 por Thomas