Implementação de Mecanismo Watchdog para Renovação de Bloqueios Distribuídos no Redis

Contexto e Diagnóstico do Problema

Ao utilizar o RedisTemplate do Spring para construir bloqueios distribuídos, é comum enfrentar situações em que operações duplicadas ocorrem em registros críticos. A causa raiz desse comportamento está diretamente ligada ao tempo de execução do bloco de código protegido. Se a tarefa demorar mais do que o tempo de expiração (TTL) configurado para a chave no Redis, o bloqueio será liberado automaticamente antes da conclusão do processo.

Consequentemente, enquanto a thread original ainda está processando a lógica de negócios, a chave de bloqueio expira. Se uma segunda thread tentar adquirir o bloqueio nesse exato momento, ela terá sucesso, resultando em execução concorrente e violando a garantia de exclusividade.

Arquitetura da Solução: Watchdog

Para mitigar esse problema sem definir um TTL excessivmaente longo (o que causaria bloqueios prolongados em caso de falha da aplicação), é necessário implementar um mecanismo de renovação automática. Inspirado no conceito de "Watchdog" (cão de guarda) utilizado por bibliotecas maduras como o Redisson, a solução consiste em monitorar a thread que detém o bloqueio.

A lógica opera da seguinte forma: antes que o tempo de expiração atual se esgote, o sistema verifica se a thread proprietária ainda está ativa. Em caso afirmativo, o TTL é estendido. Essa verificação e renovação devem ocorrer em intervalos regulares, tipicamente a cada um terço do tempo total de concessão do bloqueio.

Implementação Técnica

Abaixo está a implementação da aquisição do bloquieo, que aciona o agendamento da tarefa de renovação assim que a posse é confirmada.

// Trecho simplificado do método de aquisição de bloqueio
boolean isAcquired = Boolean.TRUE.equals(
    redisOperations.opsForValue().setIfAbsent(
        lockIdentifier, 
        threadSignature, 
        leaseDuration, 
        TimeUnit.MILLISECONDS
    )
);

if (isAcquired) {
    // Inicia o monitor de renovação após adquirir o bloqueio com sucesso
    scheduleWatchdog(lockIdentifier, threadSignature, leaseDuration);
    return true;
}

A seguir, detalhamos o método responsável por prolongar a vida útil do bloqueio. Utilizamos um ScheduledExecutorService para executar a renovação de forma periódica e não bloqueante, aliado a um script Lua para garantir a atomicidade da operação no Redis.

/**
 * Script Lua para renovação atômica.
 * Verifica se o valor atual corresponde ao identificador da thread antes de estender o TTL.
 */
private static final String LUA_RENEW_SCRIPT = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('pexpire', KEYS[1], ARGV[2]) " +
    "else " +
    "    return 0 " +
    "end";

private final ScheduledExecutorService watchdogScheduler = Executors.newSingleThreadScheduledExecutor();

/**
 * Agenda a renovação periódica do tempo de expiração do bloqueio.
 *
 * @param lockIdentifier A chave do bloqueio no Redis.
 * @param threadSignature O identificador único da thread proprietária (valor da chave).
 * @param leaseDuration O tempo total de concessão do bloqueio em milissegundos.
 */
public void scheduleWatchdog(String lockIdentifier, String threadSignature, long leaseDuration) {
    DefaultRedisScript<long> script = new DefaultRedisScript<>(LUA_RENEW_SCRIPT, Long.class);
    List<string> keys = Collections.singletonList(lockIdentifier);
    
    // Calcula o intervalo de renovação (1/3 do tempo total de concessão)
    long renewalInterval = leaseDuration / 3;

    watchdogScheduler.scheduleAtFixedRate(() -> {
        try {
            String durationStr = String.valueOf(leaseDuration);
            Long result = redisTemplate.execute(script, keys, threadSignature, durationStr);
            
            // Se o resultado for nulo ou 0, o bloqueio já foi liberado ou reassumido por outra thread
            if (result == null || result == 0) {
                // Interrompe a execução futura desta tarefa agendada
                throw new RuntimeException("Lock no longer held, stopping watchdog.");
            }
        } catch (Exception e) {
            // Lançar exceção no ScheduledExecutorService cancela as execuções futuras
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }, renewalInterval, renewalInterval, TimeUnit.MILLISECONDS);
}</string></long>

É fundamental notar que o uso de scripts Lua garante que a verificação do proprietário e a atualização do tempo de expiração ocorram como uma única operação atômica no servidor Redis. Além disso, o agendamento com ScheduledExecutorService é mais eficiente e robusto do que o uso de loops infinitos com Thread.sleep, evitando o bloqueio desnecessário de threads do pool e garantindo que a tarefa de renovação seja cancelada automaticamente quando o bloqueio não estiver mais sob posse da aplicação.

Tags: Redis spring-data-redis distributed-lock Lua java

Publicado em 6-25 22:59