O Desafio da Consistência em Sistemas Distribuídos
Em cenários de alta concorrência, como sistemas de vendas flash ou "seckill", garantir a integridade dos dados é fundamental. Quando múltiplas instâncias de um serviço tentam manipular o mesmo recurso (por exemplo, o estoque de um produto), o uso de travas locais do Java torna-se insuficiente.
Limitações de Travas em Nível de JVM
Inicialmente, poderíamos tentar resolver o problema de concorência usando mecanismos internos da linguagem, como synchronized ou ReentrantLock. No entanto, essas ferramentas operam apenas dentro de uma única instância da Java Virtual Machine (JVM).
@GetMapping("/venda/local")
public String processarVendaLocal() {
synchronized (this) {
// Recupera o estoque atual do cache
Integer estoqueAtual = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
if (estoqueAtual != null && estoqueAtual > 0) {
int novoEstoque = estoqueAtual - 1;
redisTemplate.opsForValue().set("estoque:item:101", novoEstoque);
return "Venda concluída! Restam: " + novoEstoque;
}
return "Produto esgotado.";
}
}
Em um ambiente distribuído com múltiplas instâncias atrás de um Load Balancer, cada servidor terá seu próprio objeto de trava, permitindo que diferentes processos reduzam o estoque simultaneamente, causando a venda de itens inexistentes (overselling).
Implementação com SETNX (Set if Not Exists)
Para resolver o isolamento entre diferentes nós, utilizmaos o Redis como um gerenciador de bloqueios centralizado. O comando SETNX permite definir uma chave apenas se ela não existir, funcionando como um semáforo atômico.
@GetMapping("/venda/distribuida-v1")
public String vendaComSetNX() {
String chaveBloqueio = "lock:produto:101";
String idRequisicao = UUID.randomUUID().toString();
try {
// Tenta adquirir o bloqueio com tempo de expiração (TTL) para evitar deadlocks
Boolean sucesso = redisTemplate.opsForValue()
.setIfAbsent(chaveBloqueio, idRequisicao, Duration.ofSeconds(15));
if (Boolean.FALSE.equals(sucesso)) {
return "Servidor ocupado, tente novamente.";
}
// Lógica de negócio
Integer qtd = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
if (qtd > 0) {
redisTemplate.opsForValue().set("estoque:item:101", qtd - 1);
return "Sucesso!";
}
return "Esgotado.";
} finally {
// Liberação segura: verifica se o token ainda pertence a esta requisição
if (idRequisicao.equals(redisTemplate.opsForValue().get(chaveBloqueio))) {
redisTemplate.delete(chaveBloqueio);
}
}
}
Embora funcional, esta abordagem possui vulnerabilidades: o tempo de execução da lógica pode exceder o TTL do bloqueio, ou o processo pode falhar entre a verificação e a exclusão da chave.
Solução Robusta com Redisson
A biblioteca Redisson abstrai a complexidade do gerenciamento de bloqueios. Ela oferece travas reentrantes e implementa o mecanismo de Watchdog, que renova automaticamente o tempo de vida do bloqueio anquanto a thread detentora ainda estiver processando a tarefa.
@GetMapping("/venda/redisson")
public String vendaRedisson() {
RLock trava = redissonClient.getLock("trava:produto:101");
try {
// Aguarda até 5 segundos para obter a trava, com expiração de 10 segundos
if (trava.tryLock(5, 10, TimeUnit.SECONDS)) {
Integer saldo = (Integer) redisTemplate.opsForValue().get("estoque:item:101");
if (saldo > 0) {
int saldoFinal = saldo - 1;
redisTemplate.opsForValue().set("estoque:item:101", saldoFinal);
return "Reserva confirmada. Estoque: " + saldoFinal;
}
return "Indisponível.";
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (trava.isLocked() && trava.isHeldByCurrentThread()) {
trava.unlock();
}
}
return "Falha ao processar.";
}
Vantagens do Redisson:
- Atomicidade: Utiliza scripts Lua para garantir que as operações de verificação e criação sejam executadas como uma única unidade no Redis.
- Reentrância: Permite que a mesma thread adquira o bloqueio múltiplas vezes sem se auto-bloquear.
- Watchdog: Evita que o bloqueio expire prematuramente se a lógica de negócio demorar mais que o previsto.
Considerações sobre Redlock
Em ambientes de produção com clusters Redis, o algoritmo Redlock é recomendado. Enquanto o Redisson padrão foca em um único nó master, o Redlock exige que o bloqueio seja adquirido na maioria dos nós do cluster (quorum). Isso previne problemas de consistência em situações onde o nó master falha antes de sincronizar a chave de bloqueio com seus réplicas.