Et si on jouait un peu avec Redis et Python ?
Date: 2/10/2012 | Catégories: Developpement,Open-source,Planet-libre | Tags: python,redis
Dans la mouvance NoSQL, Redis se classe dans la catégorie "data structure server" (que l'on peut traduire en Français par "serveur de dictionnaire distant"). C'est un système permettant de traiter, de manière très performante, des données sous la forme clés/valeurs. Contrairement à des solutions comme MemCache, Redis est capable de gérer la persistance des données en stockant, sur demande, son contenu sur disque.
La clé, qui peut être comparée au nom d'une variable dans un langage de programmation, permet d'identifier de manière unique la donnée. Lors de la création de votre application, vous allez rapidement vous rendre compte que le choix de cette clé n'est pas négligeable. C'est elle qui définira "la structure" de votre base de donnée.
La clé est dans la définition de la clé.
Quant aux valeurs, elles peuvent appartenir aux types suivants:
- chaînes de caractères (strings)
- compteurs numériques (note: les données numériques seront stockées dans la base sous la forme de chaîne de caractères) (atomic counters)
- listes (lists)
- tableaux (set) et tableaux ordonnés (sorted set)
- dictionnaires (hashes)
Comme on peut le voir, on retrouve la plupart des structures du langage Python. En étant un fervent admirateur, je me suis donc penché sur l'API Python Redis.
C'est parti pour un peu de bac à sable...
Installation de Redis
Pour mes tests, j'ai fait une installation standard depuis les paquets Ubuntu du serveur Redis, de l'API Python et de la CLI iPython pou illustrer mes exemples:
sudo apt-get install redis-server python-redis ipython
Les commandes de bases
La librairie Python permet d'envoyer des commandes (voir la liste des commandes ici) au serveur.
On commence par se connecter un serveur (local en écoute sur le port TCP par défaut).
J'utilise iPython pour mes tests de la librairie Python mais il est également possible de passer par la CLI Python classique.
# ipython In [1]: import redis In [2]: r = redis.Redis('localhost')
Premier exemple clés / valeur (chaîne de caractères):
In [3]: r.set("cles", "valeur") Out[3]: True In [4]: r.get("cles") Out[4]: 'valeur'
Stocker des valeurs numériques:
In [5]: r.set("clesnum", 666) Out[5]: True In [6]: r.get("clesnum") Out[6]: '666' In [7]: r.incr("clesnum") Out[7]: 667 In [8]: r.decr("clesnum") Out[8]: 666
Attention, la méthode get retourne des chaines, il faut les convertir avant de s'en servir comme des entiers.
In [9]: a = 10 In [10]: a + r.get("clesnum") --------------------------------------------------------------------------- TypeError Traceback (most recent call last) /home/nicolargo/<ipython-input-10-06c75e85d988> in <module>() ----> 1 a + r.get("clesnum") TypeError: unsupported operand type(s) for +: 'int' and 'str' In [11]: a + int(r.get("clesnum")) Out[11]: 676
Des listes comme valeurs:
In [12]: r.rpush("clesliste", "a") Out[12]: 1L In [13]: r.rpush("clesliste", "b") Out[13]: 2L In [14]: r.rpush("clesliste", "c") Out[14]: 3L In [15]: r.lrange("clesliste", 0, -1) Out[15]: ['a', 'b', 'c'] In [16]: r.lindex("clesliste", 2) Out[16]: 'c' In [17]: r.llen("clesliste") Out[17]: 3 In [22]: r.lpush("clesliste", "z") Out[22]: 4L In [23]: r.lrange("clesliste", 0, -1) Out[23]: ['z', 'a', 'b', 'c']
Pour des listes avec des valeurs uniques (set), il faut utiliser les méthodes sadd (pour ajouter) et smembers (pour récupérer le set).
In [27]: r.sadd("clesset", "a") Out[27]: True In [28]: r.sadd("clesset", "b") Out[28]: True In [29]: r.sadd("clesset", "c") Out[29]: True In [30]: r.sadd("clesset", "a") Out[30]: False In [31]: r.smembers("clesset") Out[31]: set(['a', 'c', 'b'])
Une autre structure de liste intéressante est la liste avec des valeurs uniques et ordonnées. On ajoute un troisième champs à notre couple clés/valeur qui s'appelle score (3em paramètre dans la méthode zadd). C'est ce score qui va déterminer l'ordre de la liste:
In [32]: r.zadd("clessetsort", "a", 4) Out[32]: True In [33]: r.zadd("clessetsort", "b", 1) Out[33]: True In [34]: r.zadd("clessetsort", "c", 3) Out[34]: True In [35]: r.zadd("clessetsort", "a", 5) Out[35]: False In [36]: r.zrange("clessetsort", 0, -1, withscores = True) Out[36]: [('b', 1.0), ('c', 3.0), ('a', 5.0)] In [37]: r.zrange("clessetsort", 0, -1) Out[37]: ['b', 'c', 'a']
Pour stocker un dictionnaire, c'est également très simple:
In [38]: r.hset("cleshash", "a", "A") Out[38]: 1L In [39]: r.hset("cleshash", "b", "B") Out[39]: 1L In [40]: r.hset("cleshash", "c", "C") Out[40]: 1L In [41]: r.hgetall("cleshash") Out[41]: {'a': 'A', 'b': 'B', 'c': 'C'} In [42]: r.hget("cleshash", "b") Out[42]: 'B'
Gérer l'expiration de vos données
... importance du garbage...
In [3]: r.set("key", "value") Out[3]: True In [4]: r.exists("key") Out[4]: True In [5]: r.expire("key", 10) Out[5]: True In [6]: r.exists("key") Out[6]: True Attendre 10 secondes ou plus... In [7]: r.exists("key") Out[7]: False
Egalement de manière statique avec la méthode expireat (http://redis.io/commands/expireat).
Pour être plus fin, il est aussi possible d'utiliser les "scores" avec les méthodes z* (zadd, zrangebyscore) pour supprimer les données qui ont des valeurs de clés les plus faibles (ou forte selon votre structure). Par exemple pour supprimer les données de plus faible score (< 3) de notre liste "clesetsort" créée dans le chapitre précédant:
In [37]: r.zrange("clessetsort", 0, -1) Out[37]: ['b', 'c', 'a'] In [60]: r.zremrangebyscore("clessetsort", 0, 3) Out[60]: 2 In [61]: r.zrange("clessetsort", 0, -1) Out[61]: ['a']
Les pipelines
Si Redis vous intéresse, c'est que vous avez à gérer un volume important de données. Les commandes que nous venons de voir dans le chapitre précédant sont unitaires. C'est à dire que pour une exécuter une commande, une requête (TCP) est faite au serveur Redis. Pour optimiser les performances et donc réduire le nombre de requêtes fait au serveur, nous disposons des Pipelines (lire ce billet comparatif sur le sujet).
Par exemple pour entrer 100.000 clés/valeurs dans votre serveur, il faut environ 11 secondes sur mon PC de test:
import time import redis _MAX_ITER = 100000 r = redis.Redis('localhost') print "Start bench without Pipeline" start=time.clock() for i in range(_MAX_ITER): r.set("cles%d" % i, i) stop=time.clock() print "Result: %s" % str(stop-start)
alors qu'il faut un peu de plus de 4 secondes en utilisant une pipeline (soit un gain de plus de 50%):
import time import redis _MAX_ITER = 100000 r = redis.Redis('localhost') print "Start bench with Pipeline" start=time.clock() p = r.pipeline() for i in range(_MAX_ITER): p.set("cles%d" % i, i) p.execute() stop=time.clock() print "Result: %s" % str(stop-start)
Ressources: