Locks distribuídos são essenciais em sistemas onde múltiplos processos, operando em diferentes máquinas, necessitam de acesso exclusivo a um recurso compartilhado. O Redis, com sua capacidade de gerenciar chaves e valores de forma atômica e rápida, é uma escolha popular para implementar esses mecanismos. A ideia central é utilizar uma chave no Redis para representar o lock; a posse dessa chave indica que um processo adquiriu o lock.
Abordagem 1: Utilizando SETNX e EXPIRE (com ressalvas)
Uma implementação inicial pode envolver o comando SETNX (Set if Not Exists) para adquirir o lock. Se o comando retornar 1, o processo obteve o lock. Para liberá-lo, o comando DEL é usado. No enttanto, se o processo que detém o lock falhar antes de liberá-lo, o lock nunca será liberado, levando a um deadlock.
Para mitigar isso, adiciona-se um tempo de expiração usando EXPIRE. Contudo, a sequência SETNX seguido por EXPIRE não é atômica. Se o servidor Redis falhar após SETNX mas antes de EXPIRE, o lock ficará sem expiração, resultando em deadlock.
Abodragem 2: O Comando SET com NX e EX
O Redis oferece uma solução mais robusta com o comando SET, que permite especificar as opções NX (somente definir se a chave não existe) e EX (definir tempo de expiração em segundos) em uma única operação atômica.
O formato do comando é:
SET lock.key lock.value NX EX <tempo_expiracao>
Este método resolve o problema da não atomicidade entre SETNX e EXPIRE. No entanto, um novo problema surge:
- Um processo adquire o lock com um tempo de expiração de 30 segundos.
- O processo demora mais que 30 segundos para completar a operação. O lock expira.
- Um segundo processo adquire o lock.
- O primeiro processo, ao terminar, executa
DELpara liberar o lock, mas acaba deletando o lock adquirido pelo segundo processo.
Para evitar a deleção indevida, o valor associado à chave do lock deve ser um identificador único do processo (e.g., ID da thread). Antes de deletar, o processo verifica se o valor atual da chave corresponde ao seu próprio identificador. Se corresponder, ele deleta. Para garantir a atomicidade desta verificação e deleção, pode-se utilizar scripts Lua.
Abordagem 3: Implementação Baseada em SETNX, GET, GETSET e Scripts Lua
Esta abordagem tenta gerenciar a aquisição e liberação de locks de forma mais granular:
- Tentar adquirir o lock com
SETNXe armazenar o tempo de expiração + tempo atual como valor. - Se falhar, usar
GETpara obter o tempo de expiração antigo. Comparar com o tempo atual. Se expirado, tentar atualizar o valor comGETSET, retornando o valor antigo. - Comparar o valor retornado por
GETSETcom o valor que se esperava ter. Se forem iguais, o lock foi adquirido com sucesso. - Ao liberar, verificar se o tempo de processamento é menor que o tempo de expiração do lock e se o processo ainda detém o lock antes de deletá-lo.
Problemas potenciais:
- Dependência da sincronização de tempo entre servidores.
- Conflitos em
GETSETse múltiplos processos tentarem adquirir um lock expirado simultaneamente. - Possibilidade de múltiplos processos manterem o lock se o tempo de processamento exceder a expiração (resolvível com "renovação de lock").
Abordagem 4: RedLock - Alta Disponibilidade com Múltiplos Nós Redis
Para superar as limitações de um único nó Redis (ponto único de falha), o algoritmo RedLock utiliza um conjunto de nós Redis independentes (geralmente um número ímpar, como 5). A aquisição de um lock requer sucesso em uma maioria desses nós.
Princípio de Aquisição:
- Obter o tempo atual em milissegundos e definir um Time-To-Live (TTL) para o lock. O TTL deve ser maior que o tempo de execução esperado + tempo de rede + possível drift de relógio.
- Tentar adquirir o lock sequencialmente em cada nó Redis usando a mesma chave e um valor único para o cliente. Definir um timeout curto para cada requisição individual, menor que o TTL.
- Se um número maior que a metade dos nós retornar sucesso, o lock é considerado adquirido.
- O tempo de vida efetivo do lock é calculado como
TTL - tempo\_aquisição - drift\_relógio. - Se a aquisição falhar (não obter maioria ou demorar muito), liberar o lock em todos os nós onde ele foi adquirido.
- Em caso de falha na aquisição, o cliente deve esperar um tempo aleatório antes de tentar novamente, com um limite de tentativas.
Considerações sobre RedLock:
- Alta Disponibilidade: Melhora a resiliência comparado a um único nó.
- Persistência e Recuperação: A segurança do lock depende da configuração de persistência do Redis. Sem persistência ou com AOF configurado de forma a perder dados em caso de falha rápida, a exclusividade pode ser comprometida após reinicializações.
- "Reinicialização Atrasada" (Delayed Restart): Uma estratégia para mitigar problemas de persistência é atrasar a reinicialização de nós Redis após uma falha, permitindo que os locks existentes expirem naturalmente.
Abordagem 5: Redisson e o Mecanismo "Watchdog"
A biblioteca Redisson para Java oferece implementações robustas de locks distribuídos, incluindo uma versão do RedLock e um mecanismo chamado "Watchdog".
Mecanismo Watchdog:
Quando um lock é adquirido com sucesso, o Redisson inicia um thread de "guarda" (watchdog). Este thread monitora o tempo de expiração do lock. Se o lock estiver prestes a expirar e o processo ainda o detiver, o watchdog o renovará automaticamente, estendendo seu tempo de vida. Isso resolve o problema de processos que demoram mais que o tempo de expiração original.
Implementação do Redisson (Exemplos):
O Redisson utiliza scripts Lua para garantir a atomicidade das operações de aquisição e liberação, incluindo a lógica de reentrada e a geração de IDs únicos para os clientes.
Exemplo de Aquisição (simplificado via script Lua):
-- Verifica se a chave do lock existe
if (redis.call('exists', KEYS[1]) == 0) then
-- Se não existe, define o campo com o ID do cliente e o valor 1
redis.call('hset', KEYS[1], ARGV[2], 1);
-- Define o tempo de expiração do lock
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil; -- Sucesso
end;
-- Se a chave existe, verifica se o ID do cliente é o mesmo
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- Se for o mesmo, incrementa o contador de reentrada e renova a expiração
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil; -- Sucesso (reentrada)
end;
-- Se a chave existe mas o ID é diferente, retorna o tempo de vida restante
return redis.call('pttl', KEYS[1]);
Exemplo de Liberação (simplificado via script Lua):
-- Verifica se a chave do lock existe
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('publish', KEYS[2], ARGV[1]); -- Publica mensagem de desbloqueio
return 1; -- Lock não existia, já liberado
end;
-- Verifica se o ID do cliente corresponde
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil; -- ID não corresponde, não é o dono do lock
end;
-- Decrementa o contador de reentrada
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- Se o contador for maior que 0, apenas renova a expiração
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0; -- Lock reentrante, não totalmente liberado
else
-- Se o contador for 0, deleta a chave e publica mensagem de desbloqueio
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1; -- Lock liberado com sucesso
end;
return nil;
Uso com Redisson:
Config config = new Config();
// Configuração para Sentinel ou Cluster
config.useSentinelServers()
.addSentinelAddress("127.0.0.1:6369", "127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("myMaster")
.setPassword("secret");
RedissonClient redissonClient = Redisson.create(config);
RLock redLock = redissonClient.getLock("myDistributedLock");
try {
// Tenta adquirir o lock por 500ms, com expiração automática em 10000ms
boolean acquired = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (acquired) {
// Executar lógica crítica aqui
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (redLock.isLocked()) {
redLock.unlock();
}
}
Desvantagem Fundamental da Replicação Assíncrona:
Mesmo com abordagens como RedLock ou configurações Master-Slave/Sentinel, um cenário crítico pode ocorrer: um nó Master pode falhar logo após a escrita de um lock, mas antes que essa escrita seja replicada para o Slave. Quando o Slave se torna o novo Master, ele não terá o lock, permitindo que outro cliente o adquira, violando a exclusividade.