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.