Utilisateurs en ligne avec le sorted set de Redis


Beaucoup de gens sous exploitent Redis en se cantonnant à un usage clé/valeur, voire pour les plus aguérris à la liste ou le hash.

Mais derrière sa simplicité d’usage, l’outil possède une grande richesse de fonctionnalités comme le PUB/SUB, l’hyperloglog et les sorted sets.

C’est de ces derniers dont on va parler.

En effet, dernièrement j’ai dû mettre en place un petit compteur des visiteurs en ligne, et c’est typiquement quelque chose qui fusille une base de données car ça fait une écriture ou une lecture à chaque changement de vue.

Redis est idéal pour ça, le truc tenant 100 000 écritures / seconde, et perdre 4 secondes d’historique du compteur en cas de crash est le cadet de mes soucis.

Pour compter les visiteurs, il nous faut non seulement un moyen d’incrémenter le compteur, mais aussi de le décrémenter quand le visiteur n’est plus en ligne. J’utiliserais WAMP, Crossbar pourrait me donner l’information à tout moment via l’API meta, mais sur un site HTTP only il n’y a aucun événement qui nous signale “l’utilisateur n’est plus connecté”.

Le seule moyen de savoir qu’un utilisateur n’est plus connecté est de garder une trace de lui et de la faire expirer quand on n’a pas de nouvelle depuis un temps supérieur à une durée raisonnable (calculée avec précision ) 10 minutes par l’INDMS).

Problème, on peut bien faire expirer des clés avec Redis, mais on ne peut pas lister ces clés (la fonction keys() le fait mais avec des perfs désastreuses et l’auteur du logiciel la conseille uniquement pour regarder l’état de son serveur depuis la console). D’un autre côté on a des listes, mais l’expiration concerne toute la liste, pas juste un élément.

Une manière de contourner le problème est d’utiliser un sorted set, une structure de données Redis qui a les caractéristiques suivantes :

  • Clé/valeur, mais la clé est forcément du texte, et la valeur forcément un chiffre.
  • Les clés sonts uniques.
  • Les valeurs peuvent être en doublons.
  • Les paires sont toujours ordonnées peu importe le nombre d’insertions.
  • L’ordre se fait sur les valeurs, qu’on appelle le score.

En gros, c’est comme un dictionnaire, mais ordonné, qui ressemble à :

{
    'cle': 789789
    'cle2': 78999
    'cle3': 6
}

Comme toutes les structures de données un peu exotiques, on se demande à quoi ça sert d’avoir des caratéristiques aussi précises et ce qu’on peut bien en faire.

La réponse est comme d’hab, “plein de choses”, mais nous on va en faire un compteur :

import datetime
 
import redis
 
# connection à redis
con = redis.StrictRedis()
 
# On appelle cette vue avec ajax, ignorant les
# utilisateurs sans JS qui de toute façon sont des 
# anarco communistes qui ne rapportent
# pas d'argent et fument des joins
# Cette partie dépend de votre framework
@ajax('/counter')
def add_user_count(request):
 
    # la clé d'accès à notre sorted set
    counter_key = "users:online"
 
    # un id unique pour notre visiteur. Ca dépend du
    # framework web
    user_id = request.session.id
 
    # Calcul du timestamp de maintenant et d'il y a 10 minutes
    # car ils vont nous servir de score max et min.
    now = (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds()
    ten_minutes_ago = now - (60 * 10)
 
    # On ajoute l'utilisateur dans le sorted set. S'il existait déjà, son
    # ancienne valeur est remplacée, sinon elle est créée. Comme c'est
    # forcément le plus récent timestamp, il est tout en haut du sorted set
    con.zadd(counter_key, now, user_id)
 
    # Le sorted set ressemble donc à ça :
 
    # {
 
    #     "userid": 809809890, # timestamp de dernière requete
    #     "userid2": 809809885, # timestamp plus ancien
    #     "userid3": 809809880 # timestamp encore plus ancien
    #     ...
    # }
 
    # On retire toutes les entrées du sorted set avec un score
    # qui est plus petit que le timestamp d'il y a 10 minutes
    # histoire de pas saturer la mémoire.
    con.zremrangebyscore(online_user_key, 0, ten_minutes_ago)
 
    # Et on récupére le nombre de visiteurs en ligne entre maintenant
    # et 10 minutes.
    visitors = con.zcount(online_user_key, ten_minutes_ago, now)
 
    return visitors

Notez que toutes ces étapes sont très rapides, que ce soit l’insertion, la suppression ou le compte du total grâce aux fantastiques perfs de Redis. J’aime Redis. Je suce la bite de Redis goulument.

La précision n’est pas parfaite, mais compter les utilisateurs est plus un art (divinatoire) qu’une science et avoir un chiffre précis à 2, 3% prêt est suffisant pour nous.

7 thoughts on “Utilisateurs en ligne avec le sorted set de Redis

  • Artscoop

    […] Goulûment !
    Précisément !
    J’utilise une technique très similaire à base de sets, de Redis et d’un compteur, mais aussi de Celery (pour décrémenter le compteur et vider le set à intervalles réguliers). Et ça marche aussi du fou de diou.
    Mais j’avoue que le Remrangebyscore… c’est juste magnifique, moi qui faisais ça manuellement…
    Merci Sam !

  • Sam Post author

    Google analytics est beaucoup plus puissant que ça, et aussi beaucoup plus invasif. Mais on ne peut que lire les données dans l’admin, pour l’afficher sur son site il faut payer (http://stackoverflow.com/questions/13839430/real-time-visitors-from-google-analytics-how-to-get-it). Avec cette méthode, on peut afficher le compteur, on a la main sur sa stack et ses données, et on ne track pas ses visiteurs. Les deux méthodes sont intéressantes, ça dépend de l’effet qu’on souhaite obtenir.

  • ben

    ten_minutes_ago = now – (60 * 60 * 10)

    ça fait plutôt 10 heures si ne me trompe pas

  • Sam Post author

    Sur le blog aucune idée, on a pas de compteur instantané.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.