Filtro de Bloom de Conteo Mejorado con Seguridad Concorrencia y Capacidad de Eliminación

El filtro de Bloom es una estructura de datos compacta que permite verificar pertenencia a un conjunto con una tasa de falsos positivos configurable. Su variante de conteo extiende esta funcionalidad al reemplazar bits por contadorres, habilitando la eliminación segura y la estimación del tamaño del conjunto. Esta implementación incorpora mecanismos de seguridad concorrencial mediante bloqueos de lectura-escritura, cálculo automático de parámetros óptimos y persistencia mediante serialización.

Objetivos de Diseño

La versión mejorada se propone superar las limitaciones estándar:

  • Eliminación segura de elementos sin afectar otros registros.
  • Control de acceso concorrencial con hilos múltiples.
  • Estimación del número de elementos insertados.
  • Evaluación dinámica de la tasa de falsos positivos basada en la carga actual.
  • Configuración flexible de funciones hash y cálculo óptimo de dimensiones.
  • Soporte para persistencia mediante serialización/deserialización.

Estructura de Datos Principal

El núcleo del filtro utiliza un arreglo de contadores de precisión limitada y un conjunto de funciones hash independientes:


private final short[] contadores; // Reemplaza el arreglo de bits para permitir incrementos/decrementos
private volatile int insercionesTotales = 0; // Registro aproximado de inserciones
private final ReadWriteLock bloqueo = new ReentrantReadWriteLock(); // Control de concurrencia
private final List<HashFunction> funcionesHash; // Funciones hash múltiples

La elección de short balancea uso de memoria y rango de valores (máximo 65535 por posición). El campo insercionesTotales se actualiza cuando un contador cambia entre 0 y 1.

Operaciones Fundamentales

Insertar Elementos

Al insertar, se incrementan los contadores en las posiciones hash. Si un contador pasa de 0 a 1, se considera una nueva posición ocupada:


public void insertar(String elemento) {
    bloqueo.writeLock().lock();
    try {
        for (HashFunction hf : funcionesHash) {
            int posicion = Math.abs(hf.hashString(elemento, StandardCharsets.UTF_8).asInt()) % m;
            contadores[posicion]++;
            if (contadores[posicion] == 1) {
                insercionesTotales++;
            }
        }
    } finally {
        bloqueo.writeLock().unlock();
    }
}

Eliminar Elementos

La eliminación decrementa contadores solo si son positivos, previniendo valores negativos:


public void eliminar(String elemento) {
    bloqueo.writeLock().lock();
    try {
        for (HashFunction hf : funcionesHash) {
            int posicion = Math.abs(hf.hashString(elemento, StandardCharsets.UTF_8).asInt()) % m;
            if (contadores[posicion] > 0) {
                contadores[posicion]--;
                if (contadores[posicion] == 0) {
                    insercionesTotales--;
                }
            }
        }
    } finally {
        bloqueo.writeLock().unlock();
    }
}

Consulta y Estadísticas

La verificación de pertenencia requiere que todos los contadores hash sean positivos. Las estadísticas se calculan derivando del número de inserciones totales:


public boolean contiene(String elemento) {
    bloqueo.readLock().lock();
    try {
        for (HashFunction hf : funcionesHash) {
            int posicion = Math.abs(hf.hashString(elemento, StandardCharsets.UTF_8).asInt()) % m;
            if (contadores[posicion] == 0) return false;
        }
        return true;
    } finally {
        bloqueo.readLock().unlock();
    }
}

public int estimarElementos() {
    bloqueo.readLock().lock();
    try {
        return (int) (insercionesTotales / (double) k);
    } finally {
        bloqueo.readLock().unlock();
    }
}

public double tasaFalsosPositivos() {
    bloqueo.readLock().lock();
    try {
        double n = estimarElementos();
        return Math.pow(1 - Math.exp(-k * n / m), k);
    } finally {
        bloqueo.readLock().unlock();
    }
}

Cálculo de Parámetros Óptimos

Las dimensiones del filtro (tamaño del arreglo m y número de funciones hash k) se determinan matemáticamente para minimizar espacio y falsos positivos:


private int tamanoOptimo(int n, double p) {
    return (int) (-n * Math.log(p) / (Math.pow(Math.log(2), 2)));
}

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

En el constructor se aplica un margen de seguridad del 20% al tamaño calculado para mejorar tolerancia a variaciones en la carga.

Estrategia de Funciones Hash

Se generan múltiples funciones hash mediante semillas diferentes para reducir colisiones:


for (int i = 0; i < k; i++) {
    HashFunction hf = Hashing.murmur3_128(0xABCDEF + i);
    funcionesHash.add(hf);
}

Seguridad Concorrencial

El uso de ReentrantReadWriteLock permite múltiples lecturas concurrentes mientras bloquea escrituras en exclusión mutua. Esto garantiza consistencia en entornos multihilo con un impacto mínimo en el rendimiento.

Persistencia y Serialización

La implementación soporta serialización mediante la interfaz Serializable, permitiendo guardar y restaurar el estado del filtro en almacenamiento persistente. Esto es esencial para servicios de larga duración o actualizaciones sin reinicio.

Validación mediante Pruebas

Se incluyen pruebas que verifican el comportamiento bajo operaciones de alta frecuencia y concurrencia:


public static void main(String[] args) throws InterruptedException {
    FiltroBloomConteoAvanzado filtro = new FiltroBloomConteoAvanzado(1000, 0.01);
    // Inserción y eliminación repetida para un elemento
    String item = "elementoPrueba";
    for (int i = 0; i < 1000; i++) filtro.insertar(item);
    System.out.println("Presente tras inserciones: " + filtro.contiene(item));
    for (int i = 0; i < 1000; i++) filtro.eliminar(item);
    System.out.println("Presente tras eliminaciones: " + filtro.contiene(item));

    // Prueba de concurrencia con múltiples hilos
    ExecutorService ejecutor = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 5000; i++) {
        String elem = "item" + (i % 100);
        if (i % 3 != 0) ejecutor.execute(() -> filtro.insertar(elem));
        else ejecutor.execute(() -> filtro.eliminar(elem));
    }
    ejecutor.shutdown();
    while (!ejecutor.isTerminated()) Thread.sleep(100);

    // Resultados
    System.out.println("Elementos estimados: " + filtro.estimarElementos());
    System.out.println("Factor de carga: " + String.format("%.2f%%", filtro.cargaActual() * 100));
    System.out.println("Tasa de falsos positivos: " + filtro.tasaFalsosPositivos());
}

Escenarios de Aplicación

Este filtro es adecuado para entornos que requieren operaciones rápidas de pertenencia con soporte para modificaciones:

  • Prevención de caché negativo en sistemas distribuidos.
  • Filtros de listas negras con soporte para eliminación.
  • Deduplicación en procesamiento de datos a gran escala.
  • Verificcaión de tareas en sistemas de concurrencia.

Consideraciones de Rendimiento

Aunque el filtro mejora funcionalidades, se debe monitorear el factor de carga y reconstruir periódicamente en escenarios de alta rotación para mantener la precisión. La concurrencia se gestiona mediante bloqueos, por lo que se recomienda limitar el número de hilos activos en entornos con restricciones de recursos.

Etiquetas: bloom-filter thread-safety java guava counting-bloom-filter

Publicado el 6-7 21:59