Implementação de Trava Distribuída com Redis e Soluções para Problemas Comuns

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:

  1. 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.
  2. 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.

Tags: Redis TravaDistribuída Lua Redisson SpringDataRedis

Publicado em 9-18 03:14