Background Image
TECNOLOGÍA

Mapas de bits de Redis: Almacenamiento de estado en lugares pequeños

May 3, 2022 | 5 Minuto(s) de lectura

Redis es un popular almacén de datos en memoria de código abierto que soporta todo tipo de estructuras de datos abstractas. En este post y en un proyecto Java de ejemplo que lo acompaña, voy a explorar dos grandes casos de uso para una de esas estructuras de datos: los mapas de bits. Los dos casos de uso: determinar "hecho" en un sistema asíncrono y recoger métricas de actividad distintas.

El código fuente del proyecto Java de ejemplo está disponible en GitHub aquí: https://github.com/burkaa01/redis-bitmap

¿Qué es un mapa de bits?

En un mapa de bits, cada bit individual de una matriz de bits (0 ó 1) se utiliza para representar un estado. Esto desbloquea el potencial de representar una gran cantidad de estado en una cantidad extremadamente pequeña de espacio (algo que realmente vale la pena explorar cuando el espacio es un bien escaso y hay una gran cantidad de estado que representar).

Para Redis Bitmaps, Redis utiliza cadenas para almacenar estos datos binarios y proporciona un conjunto de comandos que tratan estas cadenas binarias como mapas de bits. Estos comandos hacen posible establecer y obtener cualquier bit en una cadena Redis con una complejidad de tiempo constante (otra razón atractiva para explorar los siguientes casos de uso potenciales).

Primer caso de uso: realizado en un sistema asíncrono

En primer lugar, supongamos que existe una tarea asíncrona compuesta por un número fijo de pasos. Cada paso se procesa en paralelo y se debe realizar un seguimiento de cuándo finaliza cada paso para determinar cuándo se ha "completado" la tarea.

Con un mapa de bits de Redis, cuya clave es la cadena id de tarea, se puede realizar un seguimiento del estado de cada paso volteando un bit (de 0 a 1) en el desplazamiento correspondiente a cada número de paso cuando el paso se realiza. Si el bit en el que el desplazamiento es igual al número total de pasos de la tarea se voltea primero, entonces la tarea en su conjunto está terminada cuando todos los bits anteriores a ese bit de desplazamiento final también se han volteado.

Por ejemplo, si una tarea tiene 4 pasos, al voltear el bit del offset 4 (de 0 a 1) se obtiene el siguiente estado inicial: 00001000 (en este ejemplo, los desplazamientos y los números de paso se basan en cero)

Ahora, cuando se realiza el paso número 2, el estado es el siguiente: 00101000 

Cuando los pasos 0 y 3 están hechos, el estado se ve así: 10111000 

Y, finalmente, cuando el paso número 1 se hace, el estado se parece a esto: 11111000 (y porque todos los bits antes del bit final en el offset 4 han sido volteados - la tarea está hecha)

Ahora, echemos un vistazo al proyecto Java de ejemplo que he mencionado (https://github.com/burkaa01/redis-bitmap). Para ambos casos de uso todo comienza en eltrigger-app. En latrigger-apphay untriggerDone(y una definición de API) enTriggerController.javaque activará una tarea de ejemplo. Elcountrepresenta el número de pasos de la tarea de ejemplo activada.

La primera vez que el proyecto interactúa con el mapa de bits de Redis (a través del cliente de Jedis) es en ese momentotriggerDone. Aquí es donde se invierte el desplazamiento de bits igual al número total de pasos de la tarea (tal y como se ha descrito anteriormente):

// poner el bit "last" a true, inicializando un marcador de fin de pasos para esta tarea 

// antes de que esta tarea se considere terminada, todos los bits anteriores a este "último" bit también deben ponerse a verdadero 

Jedis jedis = nuevo Jedis(redisConnectionInfo);

jedis.setbit(taskId, count, true);

A continuación, el método pasa a emitir peticiones para cada paso individual en paralelo a la API definida endone-app. En el ejemplo paso a paso del estado anterior, había una parte importante de la lógica que pasé por alto: ¿cómo se determina exactamente que todos los bits anteriores a nuestro bit final se han volteado?

Esa lógica está en la funciónisDoneenDoneController.javay se parece a esto:

privatestaticboolean isDone(Jedis jedis, String task) {

    // obtiene el índice del bit falso más pequeño para determinar si la tarea está hecha 

Long leftMostZero = jedis.bitpos(tarea, falso);

  

    // contar el número de bits verdaderos, es decir, el número de pasos realizados 

    long count = jedis.bitcount(tarea);

  

    // comprueba si se ha realizado 

    boolean hecho = (leftMostZero == null || leftMostZero < 0 || leftMostZero == count);

    si (hecho) {

LOGGER.info("Realizados todos los {} pasos de la tarea: {}", count - 1, task);

}

    return hecho;

}

Tres cosas hacen posible esta determinación:

  1. El comando Redis BITPOS que devolverá la posición del primer bit puesto a 0

  2. El comando Redis BITCOUNT que contará el número de bits establecidos

  3. El hecho de que en la aplicacióntrigger-appel primer bit volteado fue el bit donde el offset es igual al número total de pasos.

Pon estas tres cosas juntas y sabrás que si no encuentras un BITPOS 0 o si ese BITPOS 0 es igual al BITCOUNT, la tarea está hecha.

Echemos un último vistazo a ese ejemplo inicial paso a paso del estado:

En el estado inicial, BITPOS 0 es 0 y BITCOUNT es 1 (la tarea no está hecha): 00001000 

Cuando se completan los pasos 0, 2 y 3, BITPOS 0 es 1 y BITCOUNT es 4 (la tarea aún no se ha completado): 10111000 

Pero finalmente, cuando el paso número 1 está hecho, BITPOS 0 y BITCOUNT son ambos iguales a 5 (¡la tarea está hecha!): 11111000 

Segundo caso de uso: recopilación de métricas de actividad distintas

Pasemos al segundo caso de uso: recopilar distintas métricas de actividad. El primer caso de uso describía cómo voltear un bit en una posición única dado un identificador (los bits se volteaban cuando se realizaba cada paso) y el comando BITCOUNT se utilizaba para el comandoisDonede la tarea en su conjunto. Esos dos comandos son todo lo que necesitarás para contar y rastrear el número de usuarios únicos en este caso de uso.

Echemos un vistazo aDistinctController.javadel proyecto de ejemplo endistinct-app. Este ejemplo es bastante simple, nuestra API toma el intuserIdy establecemos el bit correspondiente así

// establecemos el bit a true, que da cuenta del usuario distinct 

jedis.setbit(REDIS_KEY, userId, true);

Y si el bit ya estaba volteado asíuserId(es decir, este usuario no es nuevo/único) el recuento registrado a continuación permanece igual:

// cuenta el número de bits verdaderos, es decir, el número de usuarios distintos 

long count = jedis.bitcount(REDIS_KEY);

LOGGER.info("{} usuarios distintos", count);

Al igual que en el caso de uso anterior, si quieres probar este caso de uso por ti mismo, mira enTriggerController.java. La direccióntriggerDistinct(y la definición de la API) activará una cantidad variable de actividad "userId" y el métododistinct-apphará un seguimiento del número de usuarios únicos.

En conclusión

Quiero hacer hincapié en que los mapas de bits Redis no se adaptan a todos los problemas, pero creo que es una herramienta inteligente para tener en la caja de herramientas cuando el tiempo y el espacio (especialmente el espacio) es un bien escaso y tienes un montón de estado para rastrear. Espero que este artículo, el proyecto Java de ejemplo (https://github.com/burkaa01/redis-bitmap) y un par de casos de uso simulados hayan sido suficientes para ilustrarlo.

¿Tienes alguna pregunta sobre este martes técnico? Ponte en contacto con nosotros.

Tecnología

Reflexiones más recientes

Explore las entradas de nuestro blog e inspírese con los líderes de opinión de todas nuestras empresas.
Asset - Image 1 Data Storage in a Concurrent World 
DATOS

Data Storage in a Concurrent World 

Data storage and event ordering in concurrent systems can spark challenges, but there are ways to be prepared.