Bloqueios Distribuídos com Redis

// Define o valor de uma chave com expiração em segundos
SET chave valor EX 10

// Define o valor de uma chave com expiração em milissegundos
SET chave valor PX 1000

// Define tempo de expiração em segundos
EXPIRE nome_chave tempo_segundos

// Define tempo de expiração em milissegundos
PEXPIRE nome_chave tempo_milissegundos

// Retorna o tempo restante até a expiração da chave
TTL nome_chave

// Define valor sem sobrescrever, apenas se a chave não existir
SETNX chave valor

// Define valor com tempo de expiração fixo (em segundos)
SETEX chave segundos valor

// Define valor com tempo de expiração fixo (em milissegundos)
PSETEX chave milissegundos valor

Solução Básica: Uso do comando SETNX

Implementação simples com riscos de inconsistência:

public class BloqueioSimples {
    private Jedis cliente;
    private String chaveBloqueio;

    public BloqueioSimples(Jedis cliente, String chave) {
        this.cliente = cliente;
        this.chaveBloqueio = chave;
    }

    public boolean tentarBloquear() {
        Long resultado = cliente.setnx(chaveBloqueio, "bloqueado");
        if (resultado == 1L) {
            cliente.expire(chaveBloqueio, 30); // Tempo de vida de 30s
            return true;
        }
        return false;
    }

    public void liberar() {
        cliente.del(chaveBloqueio);
    }
}

Problemas identificados:

  • Operações não atômicas: setnx e expire podem ser interrompidas.
  • Liberação incorreta: qualquer cliente pode remover o bloqueio.
  • Não suporta reentrada: threads repetidas falham ao tentar adquirir.

Solução Melhorada: Comando SET Atômico com Lua

Garante atomicidade na aquisição e liberação segura:

public class BloqueioAtômico {
    private Jedis cliente;
    private String chaveBloqueio;
    private String identificadorCliente;

    public BloqueioAtômico(Jedis cliente, String chave) {
        this.cliente = cliente;
        this.chaveBloqueio = chave;
        this.identificadorCliente = UUID.randomUUID().toString();
    }

    public boolean tentarBloquear(int segundosExpiração) {
        String resultado = cliente.set(
            chaveBloqueio,
            identificadorCliente,
            SetParams.setParams().nx().ex(segundosExpiração)
        );
        return "OK".equals(resultado);
    }

    public boolean liberar() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) else return 0 end";
        Object resultado = cliente.eval(script, 
            Collections.singletonList(chaveBloqueio), 
            Collections.singletonList(identificadorCliente));
        return resultado.equals(1L);
    }
}

Melhorias principais:

  • Comando SET atômico com NX e EX.
  • Script Lua garante que só o dono da chave possa removê-la.
  • Identificador único por cliente evita exclusão acidental.

Limitações ainda presentes:

  • Dificuldade em renovar o bloqueio durante execução prolongada.
  • Risco de falha única no servidor Redis.
  • Tempo de execução da aplicação pode exceder o tempo de validade do bloqueio.

Solução Alta Disponibilidade: Algoritmo RedLock

Implementação distribuída com tolerância a falhas:

public class RedLock {
    private List<jedis> instanciasRedis;
    private String chaveBloqueio;
    private String identificador;
    private int quorum;

    public RedLock(List<Jedis> instancias, String chave) {
        this.instanciasRedis = instancias;
        this.chaveBloqueio = chave;
        this.identificador = UUID.randomUUID().toString();
        this.quorum = instancias.size() / 2 + 1;
    }

    public boolean tentarBloquear(long timeoutMillis) {
        long inicio = System.currentTimeMillis();
        int sucesso = 0;

        for (Jedis jedis : instanciasRedis) {
            if (tentarAdquirir(jedis, timeoutMillis)) {
                sucesso++;
            }
            if ((System.currentTimeMillis() - inicio) > timeoutMillis) break;
        }

        if (sucesso >= quorum) {
            long tempoRestante = timeoutMillis - (System.currentTimeMillis() - inicio);
            return tempoRestante > 0;
        }

        // Liberar locks parciais
        for (Jedis jedis : instanciasRedis) {
            liberar(jedis);
        }
        return false;
    }

    private boolean tentarAdquirir(Jedis jedis, long expireMs) {
        try {
            String resultado = jedis.set(
                chaveBloqueio,
                identificador,
                SetParams.setParams().nx().px(expireMs)
            );
            return "OK".equals(resultado);
        } catch (Exception e) {
            return false;
        }
    }

    private void liberar(Jedis jedis) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, 
            Collections.singletonList(chaveBloqueio),
            Collections.singletonList(identificador));
    }
}
</jedis>

Requisitos de implantação:

  • Mínimo de 5 instâncias Redis idnependentes.
  • Sincronização de relógios entre nós.
  • Configuração cuidadosa do tempo limite de bloqueio.

Cenários recomendados:

  • Sistemas financeiros críticos.
  • Aplicações distribuídas com replicação cross-datacenter.

Solução Produtiva: Redisson

Framework completo com suporte nativo a bloqueios avançados:

// Configuração do cliente Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

// Obter lock com tempo de espera e renovação automática
RLock lock = redisson.getLock("transacaoPedido");
try {
    boolean obteve = lock.tryLock(100, 30, TimeUnit.SECONDS);
    if (obteve) {
        // Processamento de negócio
    }
} finally {
    lock.unlock();
}

// Encerrar conexão
redisson.shutdown();

Mecanismo de renovação automática (Watchdog):

private void renovarValidade() {
    Timeout tarefa = commandExecutor.schedule(() -> {
        if (redisClient.eval(...).equals(true)) { // Verifica posse
            expirarAssincrono(); // Renova tempo
            renovarValidade();   // Recursividade
        }
    }, internaLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}

Recursos principais:

  • Bloqueios reentrantes.
  • Implementações de bloqueios justos, múltiplos (MultiLock) e RedLock.
  • Meacnismos robustos de recuperação de falhas e reconexão.

Tags: Redis bloco distribuído RedLock Redisson Lua Scripting

Publicado em 10-18 15:05