Et si on jouait un peu avec Redis et Python ?

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:

 

Cet article Et si on jouait un peu avec Redis et Python ? est apparu en premier sur Le blog de NicoLargo.

Vus : 1311
Publié par Nicolargo : 402