Introdução ao Bloqueio Distribuído com Redisson
Este artigo examina como o Redisson implementa bloqueios distribuídos no Redis. A utilização do Redisson simplifica significativamente a criação de bloqueios, mas sua verdadeira potência reside em fornecer uma variedade de ferramentas para sistemas distribuídos, estendendo a coordenação de concorrência de uma única máquina para um ambiente distribuído.
Configuração e Exemplo Básico
Primeiro, adicione a dependência do Redisson ao seu projeto. A seguir, um exemplo sipmlificado de como adquirir e liberar um bloqueio:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
public class TesteBloqueioRedisson {
private static RedissonClient clienteRedisson;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.1.100:6379");
clienteRedisson = Redisson.create(config);
}
public static void main(String[] args) throws InterruptedException {
RLock bloqueio = clienteRedisson.getLock("atualizacaoPedido");
// Tenta adquirir o bloqueio, esperando até 100 segundos, com tempo de vida de 10 segundos.
if (bloqueio.tryLock(100, 10, TimeUnit.SECONDS)) {
System.out.println("Bloqueio adquirido com sucesso.");
}
Thread.sleep(2000);
bloqueio.unlock();
clienteRedisson.shutdown();
}
}
Princípios Internos do Bloqueio
A aquisição do bloqueio é gerenciada pelo método tryLock. Ele chama uma função interna assíncrona (tryAcquireAsync) que utiliza um script Lua para interagir com o Redis de forma atômica.
O Script Lua para Aquisição
O script Lua garante atomicidade e ipmlementa a lógica de bloqueio reentrante. Ele verifica se a chave do bloqueio já existe. Se não existir, cria um hash com a identificação da thread e define um tempo de expiração. Se já existir e pertencer à mesma thread, incrementa a contagem de reentrância. Caso contrário, retorna o TTL restante, indicando que o bloqueio está ocupado.
"if (redis.call('exists', KEYS[1]) == 0) then " +
" redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
" redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);"
O Script Lua para Liberação
A liberação do bloqueio também é atômica. O script verifica se a thread atual é a proprietária. Se for, decrementa a contagem de reentrância. Se a contagem chegar a zero, deleta a chave e publica uma mensagem no canal Pub/Sub correspondente para nottificar outras threads que estão aguardando.
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
" return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
" redis.call('pexpire', KEYS[1], ARGV[2]); " +
" return 0; " +
"else " +
" redis.call('del', KEYS[1]); " +
" redis.call('publish', KEYS[2], ARGV[1]); " +
" return 1; " +
"end; " +
"return nil;"
Tratamento de Concorrência
Quando uma thread não consegue adquirir o bloqueio imediatamente (ou seja, o script Lua retorna um TTL), ela entra em um mecanismo de espera. O Redisson subscreve ao canal Pub/Sub associado à chave do bloqueio. A thread fica bloqueada em um semáforo, esperando até que uma notificação (a publicação no Pub/Sub) seja recebida ou até que o tempo máximo de espera expire.
O Mecanismo de Watchdog (Vigia)
Para evitar que um bloqueio expire enquanto a thread ainda está processando, o Redisson possui um mecanismo de Watchdog. Se você chamar tryLock sem especificar um leaseTime (ou passar -1), o Redisson usa um tempo de vida padrão de 30 segundos (configurável). Uma tarefa de renovação é agendada. A cada 1/3 do tempo de vida (por exemplo, a cada 10 segundos se o TTL for 30s), essa tarefa envia um script Lua para estender a expiração do bloqueio no Redis, desde que a thread ainda seja a proprietária.
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(...,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
O Papel do Pub/Sub
O sistema Pub/Sub do Redis é crucial para a eficiência do bloqueio. Em vez de as threads em espera fazerem polling constante (o que consumiria muitos recursos), elas se inscrevem em um canal dedicado. Quando um bloqueio é liberado, a thread que o detinha publica uma mensagem nesse canal. Todas as threads inscritas recebem a notificação e podem então tentar adquirir o bloqueio novamente.
PUBLISH nome_do_canal mensagem_de_liberacao
O Papel dos Scripts Lua no Redis
O Redis suporta scripts Lua nativamente, o que oferece duas vantagens principais para a implementação de bloqueios:
- Atomicidade: Todo o script é executado como uma única operação indivisível no servidor Redis, eliminando condições de corrida.
- Redução de Latência de Rede: Múltiplos comandos Redis (como EXISTS, HINCRBY, PEXPIRE) são empacotados em uma única chamada de rede.
O Redis executa esses scripts através dos comandos EVAL (executa o script diretamente) ou EVALSHA (executa um script já armazenado pelo seu hash SHA1, economizando largura de banda).