Database Engineering

Maîtriser les ZSET Redis : Construire des Classements Temps Réel Évolutifs

Dans le domaine des jeux compétitifs, de l'engagement sur les réseaux sociaux et des paris sportifs en direct, la capacité d'afficher des classements précis et en temps réel n'est pas seulement une fonctionnalité, c'est une exigence fondamentale du produit. Alors que les bases de données relationnelles traditionnelles comme PostgreSQL ou MySQL peinent avec la surcharge computationnelle liée au calcul et au tri des rangs pour des millions d'enregistrements, Redis excelle dans ce domaine grâce à son architecture en mémoire et à ses structures de données spécialisées. Plus précisément, le Set Trié (ZSET) est la référence pour implémenter des classements. Cet article explore comment architecturer des systèmes de classement haute performance en utilisant Redis, en se concentrant sur l'optimisation des opérations ZSET pour les mises à jour à haute fréquence.

Pourquoi le ZSET est le moteur du classement en temps réel

À sa base, un ZSET Redis est une collection de membres de chaînes de caractères uniques, chacun associé à un score en virgule flottante. La magie réside dans la manière dont Redis maintient cette structure. En interne, Redis utilise une combinaison de tables de hachage pour les recherches en O(1) par membre et de listes sautées (skip lists) pour l'itération ordonnée par score. Cette approche à double structure permet :

  • Une complexité O(log(N)) pour l'ajout de membres et la mise à jour des scores.
  • Une complexité O(log(N)) pour trouver le rang d'un membre.
  • Une complexité O(M + log(N)) pour récupérer une plage de membres (où M est le nombre d'éléments retournés).

Pour un classement comptant 10 millions d'utilisateurs, ces complexités garantissent que l'ajout d'un nouveau score ou la récupération des 100 premiers joueurs reste instantanée, indépendamment de la taille totale de la base d'utilisateurs.

Opérations de base et implémentation du code

L'implémentation d'un classement implique trois opérations principales : mettre à jour le score d'un utilisateur, récupérer le rang d'un utilisateur et obtenir les N premiers joueurs. Voici un exemple pratique utilisant Python avec la bibliothèque cliente redis-py. Nous supposons un scénario où nous devons mettre à jour le score d'un joueur dans un jeu appelé "SpaceInvaders".

import redis

# Connexion à l'instance Redis
client = redis.Redis(host='localhost', port=6379, decode_responses=True)

# 1. Mise à jour du score
# ZADD ajoute un membre au set trié avec un score spécifique.
# Si le membre existe déjà, il met à jour le score.
def update_leaderboard(player_id, score):
    # Utilisation du drapeau NX pour n'ajouter que si n'existe pas, ou XX pour mettre à jour si existe
    # Ici, nous mettons simplement à jour/insérons le score
    client.zadd("space_invaders_rankings", {player_id: score})
    print(f"Mise à jour de {player_id} vers le score {score}")

# 2. Obtention du rang global
# RANK retourne le rang du membre, 0 étant le premier.
# WITHSCORES retourne également le score.
def get_player_rank(player_id):
    rank = client.zrank("space_invaders_rankings", player_id)
    score = client.zscore("space_invaders_rankings", player_id)
    if rank is not None:
        # Conversion de l'index 0-based en rang 1-based
        return {"rank": rank + 1, "score": score}
    return {"rank": None, "score": None}

# 3. Obtention des 10 premiers joueurs
# ZREVRANGE récupère les membres dans l'ordre décroissant des scores
def get_top_10():
    top_players = client.zrevrange("space_invaders_rankings", 0, 9, withscores=True)
    return top_players

# Exécution d'exemple
update_leaderboard("user_123", 1500.5)
print(get_player_rank("user_123"))
print(get_top_10())

Optimisation pour les mises à jour à haute fréquence

Bien que les opérations ZADD individuelles soient rapides, les environnements à haute fréquence (tels qu'une vente flash ou une finale d'esport en direct) peuvent générer des milliers d'écritures par seconde. Pour optimiser ces scénarios, envisagez les stratégies suivantes :

1. Mises à jour par lots avec PIPELINE

La latence réseau est souvent le goulot d'étranglement dans les systèmes distribués. Au lieu d'envoyer des commandes ZADD individuelles pour la mise à jour de chaque joueur, utilisez le Pipelining Redis. Cela permet au client d'envoyer plusieurs commandes au serveur sans attendre la réponse de chacune, augmentant considérablement le débit.

def batch_update_scores(updates_dict):
    # updates_dict = {'user_A': 100, 'user_B': 200, ...}
    pipe = client.pipeline()
    pipe.zadd("space_invaders_rankings", updates_dict)
    pipe.execute()
    print("Mise à jour par lots terminée avec succès")

2. Choisir le bon type de score

Par défaut, Redis utilise des nombres à virgule flottante en double précision. Cependant, si vos scores sont des entiers, vous pouvez parfois gagner en performance et en efficacité mémoire en les normalisant. Plus important encore, assurez-vous de comprendre comment les égalités de score sont gérées. Redis brise les égalités de manière lexicographique par le nom du membre. Si vous avez besoin d'une rupture d'égalité précise basée sur le temps, envisagez d'ajouter un horodatage ou un numéro de séquence unique au score (par exemple, base_score * 1000000 + timestamp). Cela garantit que les actions plus récentes sont classées plus haut si les scores sont égaux.

3. Gestion de la mémoire et éviction

Les classements peuvent croître indéfiniment. Si vous ne disposez pas de mécanisme pour tronquer la liste, votre utilisation de la mémoire Redis augmentera linéairement avec le nombre de joueurs uniques. Mettez en œuvre une stratégie de nettoyage en utilisant ZREMRANGEBYRANK pour supprimer les joueurs qui n'ont pas mis à jour leurs scores depuis longtemps ou pour maintenir la taille du set dans des limites gérables (par exemple, conserver les 10 000 premiers joueurs et supprimer le reste).

Conclusion

Les ZSET Redis fournissent une base robuste et haute performance pour construire des classements en temps réel. En tirant parti de l'efficacité des structures de données en mémoire et en optimisant les modèles d'écriture par lot, les développeurs peuvent supporter des millions d'utilisateurs simultanés avec une latence minimale. Lors de la conception de vos systèmes de classement, profilez toujours votre cas d'utilisation spécifique, en tenant compte de facteurs tels que les règles de rupture d'égalité et les limites de mémoire, pour garantir que votre solution s'adapte gracefully sous charge.

Share: