Configuração Básica do Redis
Para utilizar o Redis como mecanismo de travamento distribuído, é necessário configurar corretamente o RedisTemplate, garantindo que as chaves e valores sejam serializados adequadamente. Abaixo temos uma configuração típica usando Spring Data Redis:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
Problema na Versão 1.0: Exclusão Não Atômica
Uma abordagem inicial comum envolve definir uma chave no Redis para representar o lock e removê-la no bloco finally. No entanto, há um problema crítico: a verificação do dono da trava seguida pela exclusão não é atômica.
@RestController
public class GoodController {
private static final String LOCK_KEY = "redisLock";
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@RequestMapping("/buy")
public String buy() {
String token = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, token, Duration.ofSeconds(10));
if (Boolean.FALSE.equals(locked)) {
return "Falha ao adquirir o lock";
}
// Operações sobre estoque
String stockStr = (String) redisTemplate.opsForValue().get("goods:001");
int stock = (stockStr != null) ? Integer.parseInt(stockStr) : 0;
if (stock > 0) {
redisTemplate.opsForValue().set("goods:001", String.valueOf(stock - 1));
System.out.println("Compra bem-sucedida, estoque restante: " + (stock - 1));
} else {
System.out.println("Estoque esgotado");
}
return "Estoque esgotado ou compra concluída";
} finally {
// RACE CONDITION AQUI!
if (token.equals(redisTemplate.opsForValue().get(LOCK_KEY))) {
redisTemplate.delete(LOCK_KEY); // Pode deletar o lock de outro cliente!
}
}
}
}
O risco ocorre quando, entre a chamada get e delete, outro processo assume o controle do lock — nesse caso, o primeiro processo pode inadvertidamente liberar o lock de outro, levando a condições de corrida.
Versão 2.0: Uso de Trensação com WATCH/MULTI/EXEC
Para mitigar esse problema, pode-se usar o mecanismo de transação otimista do Redis com WATCH, MULTI e EXEC:
finally {
while (true) {
redisTemplate.watch(LOCK_KEY);
if (token.equals(redisTemplate.opsForValue().get(LOCK_KEY))) {
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
redisTemplate.delete(LOCK_KEY);
List<Object> result = redisTemplate.exec(); // Retorna null se falhar
if (result != null) {
break; // Sucesso ao liberar
}
} else {
redisTemplate.unwatch();
break;
}
}
}
Embora mais seguro, essa abordagem ainda é complexa e sensível a comportamentos inesperados sob alta concorrência ou latência.
Solução Efetiva: Script Lua para Atomicidade
O Redis garante atomicidade na execução de scripts Lua. Assim, podemos encapsular a lógica de liberação condicional em um script que só remove o lock se pertencer ao cliente atual:
finally {
Jedis jedis = RedisUtils.getJedis();
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
try {
Object result = jedis.eval(luaScript,
Collections.singletonList(LOCK_KEY),
Collections.singletonList(token));
if ("1".equals(result.toString())) {
System.out.println("Lock liberado com sucesso");
} else {
System.out.println("Falha ao liberar lock – não era mais proprietário");
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
Essa solução elimina completamente a condição de corrida durante a liberação.
Desafios Avançados: Renovação de Lock e Tolerância a Falhas
Dois problemas maiores surgem em ambientes distribuídos:
- Tempo de expiração menor que o tempo de processamento: Se o negócio demorar mais que o TTL do lock, ele expira prematuramente, permitindo acesso simultâneo.
- Replicação assíncrona do Redis: Em falhas de failover, o nó mestre pode ter perdido dados não replicados, fazendo com que locks válidos sejam perdidos — característica do modelo AP (disponibilidade antes de consistência).
Em contraste, o ZooKeeper opera no modelo CP (consistência antes de disponibilidade), onde a sincronização entre nós é obrigatória entes de confirmar operações, tornando-o mais seguro, mas menos tolerante a particionamento.
Solução Final: Redisson como Implementação Madura
Devido à complexidade de lidar manualmente com esses cenários, bibliotecas como Redisson oferecem implementações robustas de locks distribuídos com suporte a renovação automática (watchdog), semântica de reentrância e compatibilidade com clusters Redis.
@RestController
public class GoodController {
private static final String LOCK_KEY = "redisLock";
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@RequestMapping("/buy")
public String buy() {
RLock lock = redissonClient.getLock(LOCK_KEY);
lock.lock(); // Adquire o lock com renovação automática
try {
String stockStr = (String) redisTemplate.opsForValue().get("goods:001");
int stock = (stockStr != null) ? Integer.parseInt(stockStr) : 0;
if (stock > 0) {
redisTemplate.opsForValue().set("goods:001", String.valueOf(stock - 1));
System.out.println("Compra realizada, estoque: " + (stock - 1));
} else {
System.out.println("Sem estoque disponível");
}
return "Operação concluída";
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // Libera com segurança
}
}
}
}
O Redisson internamente utiliza Lua para operações atômicas e inicia uma tarefa de renovação periódica do TTL enquanto o lock estiver ativo, evitando expirações prematuras mesmo em operações longas.