Princípios de Implementação do Bloqueio Distribuído com Redisson no Redis

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:

  1. Atomicidade: Todo o script é executado como uma única operação indivisível no servidor Redis, eliminando condições de corrida.
  2. 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).

Tags: Redisson Distributed Lock Lua Scripting Redis Pub/Sub Java Concurrency

Publicado em 6-23 18:59