Implementação de Filtros de Bloom com Spring Boot e Redis

O Filtro de Bloom é uma estrutura de dados probabilística criada em 1970 por Burton Howard Bloom. Ele é composto por um vetor de bits de longo comprimento associado a múltiplas funções de hash aleatórias. O principal objetivo dessa estrutura é determinar se um elemento pertence ou não a um conjunto específico.

Características e Funcionamento

A grande vantagem do Filtro de Bloom reside na sua eficiência de espaço e velocidade de consulta, superando algoritmos de busca tradicionais em cenários de grande volume de dados. Enrtetanto, essa eficiência traz consigo duas contrapartidas: a possibilidade de falsos positivos e a complexidade na remoção de itens.

Quando um elemento é inserido, ele passa por k funções de hash que geram k posições no vetor de bits, as quais são marcadas como 1. No processo de verificação:

  • Se qualquer uma das posições mapeadas pelas funções de hash contiver o valor 0, o elemento certamente não está no conjunto.
  • Se todas as posições mapeadas contiverem o valor 1, o elemento provavelmente está no conjunto (há uma margem de erro configurável).

Limitações

  • Falsos Positivos: Existe o risco de o filtro indicar que um elemento existe quando, na verdade, os bits foram ativados por outros elementos.
  • Dificuldade de Remoção: Como múltiplos elementos podem compartilhar os mesmos bits, alterar um bit para 0 para "remover" um item pode afetar outros registros. Para resolver isso, costuma-se utilizar variações como o Counting Bloom Filter.

Implementação Prática com Spring Boot e Redis

Para implementar essa solução, utilizamos o Redis como armazenamento do vetor de bits (via operações de Bitmap) e a biblioteca Guava para auxiliar nos cálculos de hash.

1. Classe Auxiliar de Gerenciamento

Esta classe é responsável por calcular o tamanho ideal do array de bits e gerar os índices baseados nas funções de hash.


import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

public class BloomCalculador<T> {
    private final int totalHashes;
    private final int tamanhoBitSet;
    private final Funnel<T> extrator;

    public BloomCalculador(Funnel<T> extrator, int insercoesEsperadas, double taxaFalsoPositivo) {
        Preconditions.checkArgument(extrator != null, "O extrator (funnel) não pode ser nulo");
        this.extrator = extrator;
        this.tamanhoBitSet = calcularTamanhoBits(insercoesEsperadas, taxaFalsoPositivo);
        this.totalHashes = calcularQuantidadeHashes(insercoesEsperadas, tamanhoBitSet);
    }

    public long[] extrairIndices(T valor) {
        long[] indices = new long[totalHashes];
        long hash64 = Hashing.murmur3_128().hashObject(valor, extrator).asLong();
        int hashSuperior = (int) hash64;
        int hashInferior = (int) (hash64 >>> 32);

        for (int i = 1; i <= totalHashes; i++) {
            int hashCombinado = hashSuperior + (i * hashInferior);
            if (hashCombinado < 0) {
                hashCombinado = ~hashCombinado;
            }
            indices[i - 1] = hashCombinado % tamanhoBitSet;
        }
        return indices;
    }

    private int calcularTamanhoBits(long n, double p) {
        if (p == 0) p = Double.MIN_VALUE;
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    private int calcularQuantidadeHashes(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }
}

2. Serviço de Integração com Redis

O serviço utiliza o RedisTemplate para maniuplar os bits diretamente no servidor Redis.


import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class RedisBloomService {
    private final StringRedisTemplate redisTemplate;

    public RedisBloomService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public <T> void salvarNoFiltro(BloomCalculador<T> helper, String chave, T valor) {
        long[] posicoes = helper.extrairIndices(valor);
        for (long pos : posicoes) {
            redisTemplate.opsForValue().setBit(chave, pos, true);
        }
    }

    public <T> boolean verificarExistencia(BloomCalculador<T> helper, String chave, T valor) {
        long[] posicoes = helper.extrairIndices(valor);
        for (long pos : posicoes) {
            Boolean bitAtivo = redisTemplate.opsForValue().getBit(chave, pos);
            if (bitAtivo == null || !bitAtivo) {
                return false;
            }
        }
        return true;
    }
}

3. Exemplo de Utilização

Abaixo, demonstramos como configurar o filtro para IDs de pedidos com uma taxa de erro tolerável de 1%.


import com.google.common.base.Charsets;
import com.google.common.hash.Funnels;

// Configuração para 1000 elementos e 1% de margem de erro
private BloomCalculador<String> filtroPedidos = new BloomCalculador<>(
    (from, into) -> into.putString(from, Charsets.UTF_8), 
    1000, 
    0.01
);

// Adicionando um novo ID de pedido
redisBloomService.salvarNoFiltro(filtroPedidos, "blacklist:pedidos", "ORD-99821");

// Verificando se o pedido existe
boolean existe = redisBloomService.verificarExistencia(filtroPedidos, "blacklist:pedidos", "ORD-99821");

Tags: Spring Boot Redis Bloom Filter Guava Data Structures

Publicado em 7-15 01:36