Considere uma classe Pessoa onde o campo nome deve ser único no sistema.
public class Pessoa {
private int id;
private String nome; // Deve ser garantido único
private int idade;
// Getters e setters omitidos
}
Uma abordagem comum é utilizar restrições de unicidade no banco de dados. Entretanto, ao adicionar um campo deletado (indicando remoção lógica), a restrição de unicidade torna-se inviável para cenários de reutilização de nomes.
public class Pessoa {
private int id;
private String nome; // Deve ser garantido único
private int idade;
private int deletado; // 1: removido, 0: ativo
// Getters e setters omitidos
}
Nesse caso, a garantia de unicidade precisa ser implementada no código da aplicação. Em um ambiente de aplicação único (standalone), o uso de synchronized pode resolver o problema.
public void cadastrarPessoa(Pessoa pessoa) {
// 1. Busca pessoa existente pelo nome
Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
if (existente == null) {
// 2. Sincroniza pelo nome (usando pool de strings)
synchronized (pessoa.getNome().intern()) {
// 3. Verifica novamente após obter o lock
existente = buscarPessoaPorNome(pessoa.getNome());
if (existente == null) {
// 4. Persiste no banco de dados
String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
} else {
throw new IllegalArgumentException("Nome já cadastrado.");
}
}
} else {
throw new IllegalArgumentException("Nome já cadastrado.");
}
}
Porém, em um ambiente de cluster com múltiplas instâncias da aplicação, o synchronized não oferece proteção, pois cada instância mantém seu próprio monitor. Consequentemente, duas requisições simultâneas para o mesmo nome podem ser processadas por diferentes servidores, resultando em duplicidade.
2. Solução com Travas Distribuídas via Redis
Para resolver a concorrência em cluster, utiliza-se uma trava distribuída com Redis. O comando SETNX (SET if Not eXists) garante que apenas um cliente consiga definir uma chave específica, desde que ela não exista.
Fluxo típico:
- Verificar se o nome já está cadastrado no banco de dados.
- Se não existir, solicitar uma trava no Redis associada àquele nome.
- Se a trava for obtida, proceder com a inserção e, após a operação, liberar a trava.
- Se a trava não for obtida, informar que o recurso está ocupado.
Implementação usando Spring Data Redis:
public void cadastrarPessoa(Pessoa pessoa) {
// 1. Verifica existência do nome
Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
if (existente == null) {
// 2. Tenta adquirir a trava distribuída (chave única por nome, expira em 60 segundos)
String chaveTrava = "trava_nome_" + pessoa.getNome();
Boolean adquiriuTrava = stringRedisTemplate.opsForValue().setIfAbsent(chaveTrava, "bloqueado", 60, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(adquiriuTrava)) {
try {
// 3. Simulação de operação demorada
Thread.sleep(5000);
// 4. Persistência dos dados
String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
} finally {
// 5. Libera a trava no bloco finally para garantir a execução
stringRedisTemplate.delete(chaveTrava);
}
} else {
throw new IllegalArgumentException("Serviço ocupado. Tente novamente mais tarde.");
}
} else {
throw new IllegalArgumentException("Nome já cadastrado.");
}
}
3. Aperfeiçoamento: Renovação Automática da Trava
Cenários de execução prolongada podem levar à expiração da trava antes da conclusão da transação, permitindo que outra thread adquira a trava e cause inconsistência. Para mitigar isso, implementa-se um mecanismo de renovação automática através de uma thread daemon.
public void cadastrarPessoa(Pessoa pessoa) {
// Verificação e tentativa de aquisição da trava
if (existente == null) {
String chaveTrava = "trava_nome_" + pessoa.getNome();
Boolean adquiriuTrava = stringRedisTemplate.opsForValue().setIfAbsent(chaveTrava, "bloqueado", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(adquiriuTrava)) {
// Inicia thread daemon para monitorar e renovar a trava
Thread daemonRenovacao = new Thread(() -> {
while (true) {
try {
Thread.sleep(10000); // Verifica a cada 10 segundos
Long tempoExpiracao = stringRedisTemplate.getExpire(chaveTrava);
if (tempoExpiracao == -2) { // Chave não existe
break;
}
if (tempoExpiracao < 15) { // Se expirar em menos de 15 segundos
stringRedisTemplate.expire(chaveTrava, 30, TimeUnit.SECONDS); // Renova
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
daemonRenovacao.setDaemon(true);
daemonRenovacao.start();
try {
// Lógica de negócio principal
String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
} finally {
stringRedisTemplate.delete(chaveTrava); // Libera a trava
}
} else {
throw new IllegalArgumentException("Serviço ocupado.");
}
} else {
throw new IllegalArgumentException("Nome já cadastrado.");
}
}
Considerações:
- Thread Daemon: Utilizada para que a renovação ocorra em segundo plano e encerre automaticamente quando a thread principal finalizar.
- Tempo de Expiração: Essencial para evitar que travas fiquem permanentemente no Redis em caso de falhas (ex.: queda de energia).
4. Uso do Redisson para Simplificação
A implementação manual de travas distribuídas é complexa e suscetível a erros. O Redisson, um cliente Redis avançado, fornece componentes robustos como o watchdog para renovação automática de travas.
4.1 Configuração com Spring Boot
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
@Bean
public RedissonClient clienteRedis() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
// Configura o tempo de monitoramento do watchdog (20 segundos)
config.setLockWatchdogTimeout(20000);
return Redisson.create(config);
}
4.2 Exemplo de Código com Redisson
@Autowired
private RedissonClient clienteRedis;
public void cadastrarPessoa(Pessoa pessoa) throws InterruptedException {
Pessoa existente = buscarPessoaPorNome(pessoa.getNome());
if (existente == null) {
RLock trava = clienteRedis.getLock("trava_pessoa_" + pessoa.getNome());
boolean adquiriu = trava.tryLock(5, 30, TimeUnit.SECONDS); // Espera 5s, expira em 30s
if (adquiriu) {
try {
// Lógica de negócio (pode ser demorada)
String sql = "INSERT INTO pessoa(id, nome, idade) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, pessoa.getId(), pessoa.getNome(), pessoa.getIdade());
} finally {
trava.unlock(); // Libera a trava
}
} else {
throw new IllegalArgumentException("Não foi possível obter a trava.");
}
} else {
throw new IllegalArgumentException("Nome já cadastrado.");
}
}
Observação: Para que o watchdog do Redisson funcione, a trava não deve ser adquirida com um tempo de expiração explícito no método tryLock(). Utilize versões sem parâmetros de tempo ou com apenas tempo de espera.
4.3 Travas Multi-Instância com RedissonMultiLock
Em ambientes Redis com replicação (mestre-escravo), há o risco de perda de travas durante uma falha no nó mestre antes da sincronização. O RedissonMultiLock permite aplicar a trava em múltiplos nós simultaneamente (trava em cluster), aumentando a disponibilidade.
@Bean
public RedissonClient clienteRedis2() {
Config config = new Config();
config.useSingleServer().setAddress("redis://segundo-servidor:6379");
config.setLockWatchdogTimeout(20000);
return Redisson.create(config);
}
@Bean
public RedissonClient clienteRedis3() {
Config config = new Config();
config.useSingleServer().setAddress("redis://terceiro-servidor:6379");
config.setLockWatchdogTimeout(20000);
return Redisson.create(config);
}
// Exemplo de uso em serviço
@Autowired private RedissonClient clienteRedis;
@Autowired private RedissonClient clienteRedis2;
@Autowired private RedissonClient clienteRedis3;
public void operacaoCritica() throws InterruptedException {
RLock trava1 = clienteRedis.getLock("chave_operacao");
RLock trava2 = clienteRedis2.getLock("chave_operacao");
RLock trava3 = clienteRedis3.getLock("chave_operacao");
RedissonMultiLock travaMultipla = new RedissonMultiLock(trava1, trava2, trava3);
boolean adquiriu = travaMultipla.tryLock(10, 60, TimeUnit.SECONDS);
if (adquiriu) {
try {
// Execução da operação protegida
} finally {
travaMultipla.unlock();
}
}
}
5. Considerações Finais sobre Limitações
Travas distribuídas, mesmo com mecanismos avançados, estão sujeitas a falhas em cenários extremos:
- Bloqueio de threads ou do próprio watchdog durante a renovação.
- Falhas simultâneas em múltiplos nós Redis.
- Problemas de rede que impeçam a comunicação entre a aplicação e o Redis.
A implementação visa minimizar riscos, mas não elimina completamente a possibilidade de inconsistências. É crucial monitorar a infraestrutura e ter planos de recuperação.