Background Image
TECHNOLOGIE

Bitmaps Redis : Stocker l'état dans de petits endroits

May 3, 2022 | 5 Lecture minute

Redis est un magasin de données en mémoire open source populaire qui prend en charge toutes sortes de structures de données abstraites. Dans ce billet et dans un exemple de projet Java qui l'accompagne, je vais explorer deux excellents cas d'utilisation de l'une de ces structures de données : les bitmaps. Les deux cas d'utilisation : déterminer "fait" dans un système asynchrone et collecter des métriques d'activité distinctes.

Le code source de l'exemple de projet Java est disponible sur GitHub ici : https://github.com/burkaa01/redis-bitmap

Qu'est-ce qu'une image bitmap ?

Dans un tableau de bits, chaque bit individuel d'un tableau de bits (0 ou 1) est utilisé pour représenter un état. Cela permet de représenter un grand nombre d'états dans un espace extrêmement réduit (ce qui vaut vraiment la peine d'être exploré lorsque l'espace est limité et qu'il y a un grand nombre d'états à représenter).

Pour Redis Bitmaps, Redis utilise des chaînes pour stocker ces données binaires et fournit un ensemble de commandes qui traitent ces chaînes binaires comme des bitmaps. Ces commandes permettent de définir et d'obtenir n'importe quel bit dans une chaîne Redis avec une complexité temporelle constante (une autre raison intéressante d'explorer les cas d'utilisation potentiels suivants).

Premier cas d'utilisation : dans un système asynchrone

Supposons tout d'abord qu'il existe une tâche asynchrone composée d'un nombre fixe d'étapes. Chaque étape est traitée en parallèle et vous devez savoir quand chaque étape se termine pour déterminer quand la tâche est "terminée".

Avec un Bitmap Redis, dont la clé est la chaîne task id, vous pouvez suivre l'état de chaque étape en retournant un bit (de 0 à 1) au niveau du décalage correspondant à chaque numéro d'étape lorsque l'étape est terminée. Si le bit où le décalage est égal au nombre total d'étapes de la tâche est basculé en premier, alors la tâche dans son ensemble est terminée lorsque tous les bits avant ce bit de décalage de fin ont également été basculés.

Par exemple, si une tâche comporte 4 étapes, l'inversion du bit du décalage 4 (de 0 à 1) entraîne l'état initial suivant : 00001000 (dans cet exemple, les décalages et les nombres d'étapes sont basés sur zéro)

Maintenant, lorsque l'étape numéro 2 est terminée, l'état ressemble à ceci : 00101000 

Lorsque les étapes 0 et 3 sont terminées, l'état ressemble à ceci : 10111000 

Enfin, lorsque l'étape 1 est terminée, l'état ressemble à ceci : 11111000 (et parce que tous les bits avant le bit de fin au décalage 4 ont été retournés - la tâche est terminée)

Voyons maintenant l'exemple de projet Java que j'ai mentionné (https://github.com/burkaa01/redis-bitmap). Pour les deux cas d'utilisation, tout commence dans l'élémenttrigger-app. Dans l'applicationtrigger-appil y a untriggerDone(et une définition de l'API) dansTriggerController.javaqui déclenchera une tâche d'exemple. La méthodecountreprésente le nombre d'étapes de la tâche d'exemple déclenchée.

La première fois que le projet interagit avec le bitmap Redis (via le client Jedis), c'est dans cette tâchetriggerDone. C'est là que le décalage de bit égal à la valeur du paramètre "count" est calculé. C'est là que le bit offset correspondant au nombre total d'étapes de la tâche est inversé (comme décrit ci-dessus) :

// mettre le bit "last" à vrai, initialisant un marqueur de fin d'étapes pour cette tâche 

// avant que cette tâche ne soit considérée comme terminée, tous les bits précédant ce "dernier" bit doivent également être mis à vrai 

Jedis jedis = nouveau Jedis(redisConnectionInfo) ;

jedis.setbit(taskId, count, true) ;

Ensuite, la méthode émet des requêtes pour chaque étape individuelle en parallèle à l'API définie dansdone-app. Dans l'exemple d'état étape par étape ci-dessus, il y a un élément de logique important que j'ai passé sous silence : comment déterminer exactement que tous les bits avant notre bit de fin ont été retournés ?

Cette logique se trouve dans la fonctionisDonedans le fichierDoneController.javaet elle ressemble à ceci :

privatestaticboolean isDone(Jedis jedis, String task) {

    // obtient l'indice du plus petit bit erroné à utiliser pour déterminer si la tâche est terminée 

Long leftMostZero = jedis.bitpos(task, faux) ;

  

    // compte le nombre de bits vrais, c'est-à-dire le nombre d'étapes effectuées 

    long count = jedis.bitcount(task) ;

  

    // vérifie si la tâche est terminée 

    booléen done = (leftMostZero == null || leftMostZero < 0 || leftMostZero == count) ;

    si (done) {

LOGGER.info("Done with all {} steps for task : {}", count - 1, task) ;

}

    return done ;

}

Trois éléments rendent cette détermination possible :

  1. La commande Redis BITPOS qui renvoie la position du premier bit mis à 0

  2. La commande Redis BITCOUNT qui compte le nombre de bits mis à 0.

  3. Le fait que dans la commandetrigger-apple premier bit inversé est celui dont le décalage est égal au nombre total de pas.

Mettez ces trois éléments ensemble et vous saurez que si vous ne trouvez pas de BITPOS 0 ou si ce BITPOS 0 est égal au BITCOUNT, la tâche est terminée.

Jetons un dernier coup d'œil à cet exemple initial, étape par étape :

Dans l'état initial, BITPOS 0 est 0 et BITCOUNT est 1 (la tâche n'est pas terminée) : 00001000 

Lorsque les étapes 0, 2 et 3 sont terminées, BITPOS 0 est 1 et BITCOUNT est 4 (la tâche n'est toujours pas terminée) : 10111000 

Enfin, lorsque l'étape 1 est terminée, BITPOS 0 et BITCOUNT sont tous deux égaux à 5 (la tâche est terminée !): 11111000 

Deuxième cas d'utilisation : collecte de données d'activité distinctes

Passons au deuxième cas d'utilisation : la collecte de mesures d'activité distinctes. Le premier cas d'utilisation décrivait comment retourner un bit à une position unique en fonction d'un identifiant (les bits étaient retournés à chaque étape) et la commande BITCOUNT était utilisée pour la fonctionisDonepour l'ensemble de la tâche. Ces deux commandes sont tout ce dont vous aurez besoin pour compter et suivre le nombre d'utilisateurs uniques dans ce cas d'utilisation.

Jetons un coup d'œil àDistinctController.javadu projet d'exemple dans l'applicationdistinct-app. Cet exemple est assez simple, notre API prend en compte l'intuserIdet nous définissons le bit correspondant comme suit :

// mettre le bit à true, ce qui tient compte de l'utilisateur distinct 

jedis.setbit(REDIS_KEY, userId, true) ;

Et si le bit a déjà été inversé à ce moment-làuserId(c'est-à-dire que cet utilisateur n'est pas nouveau/unique), le compte enregistré ci-dessous reste le même :

// compter le nombre de bits vrais, c'est-à-dire le nombre d'utilisateurs distincts 

long count = jedis.bitcount(REDIS_KEY) ;

LOGGER.info("{} utilisateurs distincts", count) ;

Tout comme dans le cas d'utilisation précédent, si vous voulez essayer ce cas d'utilisation par vous-même, regardez dans le fichierTriggerController.java. La fonctiontriggerDistinct(et la définition de l'API) déclenchera une quantité variable d'activité "userId" et la méthodedistinct-appsuivra le nombre d'utilisateurs uniques en conséquence.

En conclusion

Je tiens à souligner que les bitmaps Redis ne conviennent pas à tous les problèmes, mais je pense qu'il s'agit d'un outil intelligent à avoir dans sa boîte à outils lorsque le temps et l'espace (en particulier l'espace) sont réduits et que vous avez beaucoup d'états à suivre. J'espère que cet article, l'exemple de projet Java fourni (https://github.com/burkaa01/redis-bitmap) et quelques cas d'utilisation fictifs ont suffi à l'illustrer.

Vous avez une question sur ce sujet du Mardi techno ? N'hésitez pas à nous contacter.

Technologie

Dernières réflexions

Explorez nos articles de blog et laissez-vous inspirer par les leaders d'opinion de nos entreprises.
Asset - Image 1 Data Storage in a Concurrent World 
DONNÉES

Data Storage in a Concurrent World 

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