Redis : pourquoi et comment ?


Redis fait partie de ces technologies tellement utiles et simples à mettre en oeuvre qu’il est facile d’oublier toutes les personnes qui ne savent toujours pas ce que c’est. D’autant plus qu’on l’associe avec beaucoup d’étiquettes : cache, queues, pub/sub, base de données, nosql… Et qu’est-ce que Redis comparé à Memcache, MongoDB, MySQL, RabbitMQ, des technos qui n’ont rien à voir et auxquelles on le compare ?

Bref, c’est pas clair tout ça.

Hello redis

D’abord, installer le bouzin.

Si vous êtes sous Linux, Redis est dans votre gestionnaire de paquets. Par exemple, sous Ubuntu :

sudo apt-get install redis-server

Pour Mac, via macport :

sudo port install redis
sudo port load redis

Je crois que brew install redis marche aussi pour les amateurs de homebrew.

Pour Windows, il y a un exe à télécharger ici.

Mais le plus fun avec redis, c’est que même quand il y a pas de binaire, c’est le truc le plus facile du monde à compiler. Et Dieu sait que je hais la compilation, donc quand je vous dis que c’est simple, c’est que c’est mega, ultra, simple.

Ensuite lui faire dire bonjour.

Utiliser Redis se fait à base de commandes, dont la liste est sur le site officiel. On peut envoyer ces commandes depuis n’importe quel langage, mais Redis fournit un shell qui permet de rentrer ces commandes directement:

$ redis-cli # lancer le shell redis
127.0.0.1:6379>  ECHO "Hello"
"Hello"

Redis comme base de données clés/valeurs expirables

L’usage de base de Redis, c’est de stocker des valeurs associées à des clés. Le serveur Redis est comme un gigantesque Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… En premier lieu, donc, on utilise Redis pour stocker des choses ainsi :

"cle1" => valeur1
"cle2" => valeur2
etc

La clé doit être une chaîne de caractères, de préférence ASCII. La valeur peut être n’importe quoi, vraiment, mais généralement c’est un gros bloc de texte, un entier ou un blob binaire.

Ca s’utilise avec les commandes SET et GET, qui peuvent, comme toutes les commandes, être tapées dans le shell de Redis :

127.0.0.1:6379> GET une_cle
(nil)
127.0.0.1:6379> SET une_cle "Une valeur"
OK
127.0.0.1:6379> GET une_cle
"Une valeur"
127.0.0.1:6379> SET une_cle 780708
OK
127.0.0.1:6379> GET une_cle
"780708"

Ce genre d’usage est surtout sollicité pour stocker des paramètres de configurations ou des compteurs.

En effet, la plupart des opérations sur les clés sont atomiques, rendant ce genre d’usage idéal. On peut même incrémenter ou décrémenter une valeur avec INCR et DECR atomiquement :

127.0.0.1:6379> INCR une_cle
(integer) 780709
127.0.0.1:6379> INCR une_cle
(integer) 780710
127.0.0.1:6379> DECR une_cle
(integer) 780709
127.0.0.1:6379>

Vous allez me dire, mais quel intérêt ? Je peux déjà faire ça avec une variable. Certes, mais Redis est accessible depuis n’importe quel programme de votre serveur. Vous pouvez donc facilement partager des valeurs entre vos processus. Par exemple, avec Django, on a souvent plusieurs workers WSGI, et Redis permet donc de partager des informations entre ces workers. Pour cette raison, on peut utiliser Redis pour créer un lock partagé.

Ainsi, un paramètre dynamique peut être changé et lu depuis tous les process de votre projet en utilisant Redis, et la valeur sera garantie d’être à jour. On peut bien entendu faire ça avec une base de données ordinaire, mais Redis a un plus : la performance.

En effet, Redis est par défaut configuré pour garder toutes les données de sa base en mémoire vive. Pour cette raison, il est très, très rapide, supportant 100000 lectures/écritures par seconde. Si vous comptez le nombre de visiteurs en ligne pour l’afficher sur chaque page, votre base de données vous dira merci de plutôt demander à Redis.

Plus encore, les clés peuvent expirer :

127.0.0.1:6379> SET une_autre_cle "Tu ne le sais pas mais tu es deja mort"
OK
127.0.0.1:6379> EXPIRE une_autre_cle 5 # cette clé expire dans 5 secondes
(integer) 1
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
(nil)
127.0.0.1:6379> GET une_autre_cle
(nil)

La limite en taille de ce qu’on peut stocker par couple est assez large.

On peut également utiliser EXPIREAT et un timestamp unique si on a une date en tête.

Cette caractéristique le rend similaire à Memcache, qui est basé sur des clés/valeurs en mémoire qui peuvent expirer. Et comme Memcache, cela fait de Redis un outil idéal pour gérer du cache : faites une opération longue, stockez-là avec une clé, mettez lui une date d’expiration et pouf, vous avez une valeur cachée globale à votre app.

Redis a néanmoins 3 différences majeures qui le sépare de Memcache :

  • Redis sauvegarde régulièrement toutes les données sur le disque. Si vous rebootez le serveur, vous perdez au pire, quelques secondes de données. Avec Memcache, vous perdez tout.
  • Memcache possède des fonctionalités de clusterisation que n’a pas encore Redis, bien que ce soit actuellement en beta.
  • Redis peut faire bien plus que du stockage clé/valeur.

Redis comme base NoSQL zarbie

Redis n’a pas besoin d’un schéma particulier à définir pour pouvoir sauvegarder ses données, mais n’est pas pour autant limité à une forme d’organisation basique. En fait, des types très proches de ceux qu’on trouve dans le langage Python sont disponibles pour stocker internalement les données, et ils sont décrits dans son excellente doc.

Liste

Une liste est juste une collection ordonnée d’éléments. On utilise toujours la logique de clé, mais au lieu d’une valeur, on a une séquence d’éléments.

Cela permet d’avoir :

cle => [
	valeur1,
	valeur2,
	...
]

Dans le shell :

127.0.0.1:6379> lpush batman na
(integer) 1
127.0.0.1:6379> lpush batman na
(integer) 2
127.0.0.1:6379> lpush batman na
(integer) 3
127.0.0.1:6379> lpush batman na
(integer) 4
127.0.0.1:6379> lpush batman na
(integer) 5
127.0.0.1:6379> lpush batman na
(integer) 6
127.0.0.1:6379> llen batman
(integer) 6
127.0.0.1:6379> LINDEX batman 3
"na"
127.0.0.1:6379> LINDEX batman 10
(nil)
127.0.0.1:6379> LRANGE batman 0 -1 # du début au dernier élément
1) "na"
2) "na"
3) "na"
4) "na"
5) "na"
6) "na"
127.0.0.1:6379> LRANGE batman 2 3
1) "na"
2) "na"
127.0.0.1:6379>

On peut faire toutes les opérations qu’on a l’habitude de faire sur des listes : récupérer un élément, en ajouter un, en retirer un, faire du LIFO, du FIFO, du pipo, etc.

Utile pour créer des files d’attente, des journaux d’évènements (pensez jeux vidéos) ou plus simplement une valeur de config plus complexe qu’une entrée.

Hash

Le Hash se comporte comme un Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… Bref, comme Redis lui-même en fait, mais sans expiration de clé. Cela permet d’avoir :

cle => {
	cle : valeur,
	cle : valeur,
}

Dans le shell :

127.0.0.1:6379> HSET scores titi 1
(integer) 1
127.0.0.1:6379> HSET scores grosminet 0
(integer) 1
127.0.0.1:6379> HSET scores tom 0
(integer) 1
127.0.0.1:6379> HSET scores jerry 1
(integer) 1
127.0.0.1:6379> HGET scores jerry
"1"
127.0.0.1:6379> HGETALL scores
1) "titi"
2) "1"
3) "grosminet"
4) "0"
5) "tom"
6) "0"
7) "jerry"
8) "1"
127.0.0.1:6379>

Notez encore une fois qu’il n’est pas utile de vérifier que la clé existe avant de rajouter une valeur dans le hash, bien qu’il y ait une commande pour le faire. Si on essaye de récupérer une clé qui n’existe pas, Redis retourne nil.

Le hash est fort pratique pour toute forme de compteurs groupés, ou juste pour mettre en cache des relations. Redis n’ayant pas de JOIN, on utilise parfois un hash pour faire le lien entre deux listes par exemple.

Set

Comme les sets en Python, le set est une collection NON ordonnée d’éléments uniques. Il ne peut pas y avoir de doublons dans un set, et vérifier si un élément fait partie d’un set est une opération très rapide. Ils permettent aussi de faire des opérations ensemblistes de manière performante, par exemple vérifier quels éléments d’un set sont ou non dans un autre set.

cle => { valeur1, valeur2, ...}

Dans le shell :

127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.1 # ajouter 2x le même ne fait rien
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.2
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.3
(integer) 1
127.0.0.1:6379> SCARD ip
(integer) 3
127.0.0.1:6379> SMEMBERS ip
1) "192.168.1.2"
2) "192.168.1.1"
3) "192.168.1.3"
127.0.0.1:6379> SADD ip:banned 192.168.1.3 # le ":" est une séparateur courant pour les clés
(integer) 1
127.0.0.1:6379> SADD ip:banned 192.168.1.10 # ip:banned est un AUTRE set
(integer) 1
127.0.0.1:6379> SINTER ip ip:banned # trouver les IP qui sont dans les 2 sets
1) "192.168.1.3"

On va utiliser les sets pour éviter les doublons (tirage au sort par exemple) ou pour vérifier facilement une appartenance (notion de groupes, de permissions, etc.).

Le set possède une variante, le set ordonné, qui associe à chaque élément du set un score. On peut changer ce score, l’incrémenter ou le décrémenter atomiquement, avoir deux scores égaux… Et au final, récupérer le set dans l’ordre ascendant ou descendant des scores.

cle => { valeur1 (1), valeur2 (3), ...}

Dans le shell :

127.0.0.1:6379> zadd participants 1 staline
(integer) 1
127.0.0.1:6379> zadd participants 1 hitler
(integer) 1
127.0.0.1:6379> zadd participants 2 "pol pot"
(integer) 1
127.0.0.1:6379> zadd participants 1999 "rainbow dash"
(integer) 1
127.0.0.1:6379> zrange participants 0 - 1
(error) ERR value is not an integer or out of range
127.0.0.1:6379> zrange participants 0 -1 
1) "hitler"
2) "staline"
3) "pol pot"
4) "rainbow dash"
127.0.0.1:6379> zrange participants 0 -1 withscores
1) "hitler"
2) "1"
3) "staline"
4) "1"
5) "pol pot"
6) "2"
7) "rainbow dash"
8) "1999"

HyperLogLog

J’ai déjà parlé de l’HyperLogLog ici et celui de Redis fonctionne pareil. Cette structure de données permettra de faire des compteurs d’éléments uniques, comme par exemple un compteur de visiteurs ou de gens connectés, qui soit approximatif (+ ou – 1%) mais prenne une taille fixe en mémoire.

Dans le shell :

127.0.0.1:6379> PFADD connectes toto
(integer) 1
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes tata
(integer) 1
127.0.0.1:6379> PFADD connectes titi
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tete
(integer) 1
127.0.0.1:6379> PFADD connectes bob
(integer) 1
127.0.0.1:6379> PFcount connectes
(integer) 6
127.0.0.1:6379>

Redis comme super pote de Python

Vu que Redis est simple à installer et à configurer, c’est un outil qu’on dégaine facilement, même pour un petit script, pas forcément pour une grosse app Web. Par exemple, je fais une analyse de mon serveur, plutôt que de stocker le résultat dans un fichier, je peux mettre tout ça dans Redis, c’est tellement pratique.

D’abord, la lib pour communiquer avec Redis est à un pip du clavier :

pip install redis

Ensuite, si vous utilisez le client StrictRedis, l’API est exactement la même que les commandes du shell :

>>> import redis
>>> r = redis.StrictRedis()
>>> r.hgetall('scores')
{b'titi': b'1', b'tom': b'0', b'jerry': b'1', b'grosminet': b'0'}

Il existe aussi des drivers asynchrones pour tornado, twisted et asyncio.

Redis comme message broker

J’aime le pub/sub, je pense qu’après mon enthousiasme pour WAMP.ws, c’est clair.

Redis met à disposition des primitives pour créer des files d’attente de messages et les lire, comme une version simplifiée de RabbitMQ ou Crossbar.io. Pour que ce soit intéressant, il nous faut deux process. D’abord un shell Python qui écoute les messages arrivant sur la file “sametmax” :

>>> import redis
>>> r = redis.StrictRedis()
>>> listener = r.pubsub()
>>> listener.subscribe(['sametmax'])
>>> for item in listener.listen():
...     print(item)
...

Le code va bloquer, et affichera quelque chose à chaque fois qu’il reçoit un message.

Du coup si je fais ceci dans le shell Redis :

127.0.0.1:6379> publish sametmax yolo
(integer) 1
127.0.0.1:6379> publish sametmax carpediem
(integer) 1
127.0.0.1:6379> publish sametmax wololo
(integer) 1
127.0.0.1:6379> publish sametmax2 oyooyo
(integer) 0
127.0.0.1:6379>

Mon shell python va afficher :

{'type': 'subscribe', 'pattern': None, 'channel': b'sametmax', 'data': 1}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'yolo'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'carpediem'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'wololo'}

Il n’a pas affiché oyooyo puisque c’est sur une autre channel.

C’est une méthode assez simple de faire du pub/sub. C’est bas niveau, il n’y a pas de RPC, il faut boucler à la main et créer soit-même la mécanique pour arrêter d’écouter ou faire du multitask, mais c’est facile pour débuter. Pour cette raison, plein de gens utilisent une solution bricolée là-dessus pour faire du pub/sub.

Et donc

Non seulement Redis est facile à installer, simple à utiliser et performant, mais en plus il est accessible depuis de nombreux langages. Ajoutez à cela ses très nombreuses fonctionnalités, et vous avez là un système qui est installé par défaut sur la plupart de mes serveurs. Par ailleurs, suivez le blog de l’auteur, ce mec est un génie. Il écrit pas souvent, mais quand il écrit, c’est passionant, et humble. C’est beau.

23 thoughts on “Redis : pourquoi et comment ?

  • foxmask

    J’aime bien quand mes questions par mail font germer de tel billet ;) Je n’attends plus que le suivant pour savoir sur quel pied danser !
    En attendant ça éclaircit beaucoup les idées.
    Si j’ai une question sur django/haystack/elasticsearch (le rebuild_index index mes fesses et encore) ; nouveau billet ? :) ou tu hais tjs haystack ?
    En tout cas ‘Merci Bernard

  • Redsmin (Redis GUI)

    Un point qui peut être bloquant pour ceux qui débutent avec Redis est que pour utiliser pubsub, il faut créer une connexion à Redis et la passer en mode subscribe et créer une autre connexion à Redis pour pouvoir publish. Car une fois une connexion passée dans en mode subscribe il est impossible de changer ce mode.

    @juke l’implémentation de pubsub dans Redis permet simplement d’échanger des messages, par contre tu peux utiliser une liste et la commande blpop (http://redis.io/commands/blpop) pour avoir une LIFO à dépilement bloquant.

    Si vous cherchez un GUI Redis en ligne complet et facile d’utilisation n’hésitez pas à tester Redsmin !

  • zed

    “c’est un gros blog de texte, un entier ou un blog binaire.”

    “Bloc” et “blob” plutôt non ? (comme l’ami de Mickey)

  • ArBaDaCarBa

    Merci pour le PFADD, je ne connaissait pas. Par contre, il semblerait que cette commande n’est pas dans le module python.

    Une petite astuce au passage, parceque ça m’a sauvé pas plus tard qu’hier pour assurer une compatibilité python 2.7 et 3.3, on peut utiliser decode_response pour éviter d’avoir des bytes partout. Pour reprendre l’exemple :

    >>> import redis
    >>> r = redis.Redis(decode_responses=True)
    >>> r.hgetall('scores')
    {'titi': '1', 'tom': '0', 'jerry': '1', 'grosminet': '0'}

  • Sam Post author

    @oxmask: je hais solr, pas haystack. Je range le billet dans mille et une idée de sujet pour le cas improbable d’absence de draft.

    @zed : oui merci.

    @ArBaDaCarBa : l’hyperloglog vient tout juste d’être ajouté à redis.

    Au passage, notez le commentaire de Redsmin (Redis GUI) qui est l’exemple d’une pub acceptable en comments : une réponse dans le sujet, utile, un lien vers un produit lié à l’article, fait poliment, sans bullshit.

  • foxmask

    @am c’etait pour rire le billet sur la question ; je sais bien que vu la quantité de brouillons… ;)

  • juke

    @redsmin : merci, effectivement je n’ai pas besoin de pubsub, juste de peupler une liste de taches lifo avec un worker de l’autre coté.

  • Benji59

    Très bon article !

    Juste une petite remarque, sur la forme:

    “Vous allez me dire, mais quelle interêt ?” => “Vous allez me dire, mais quel intérêt ?”

  • Sam Post author

    Mais non parce que c’est une conjonction masculine du preterit à l’ablatif qui… Ok, je corrige.

  • Nicolas

    L’introduction à redis que j’aurais voulu écrire :D
    Mon article sera donc un simple lien, cool !

    J’ajoute à la liste de tes exemples le push/pull (pour faire un pool de workers) avec BLPOP et RPUSH.

    Pour le stockage en mémoire des données tu dis que “par défaut” il met tout en mémoire, je ne savais pas que c’était une option, je regarderai ça…

  • Sam Post author

    On peut limiter la place en mémoire que prend Redis en mettant :

    maxmemory nombre_de_bits
    

    Dans le fichier de config. Si on atteint ce max, redis commence à shooter des clés.

    On peut aussi mettre une partie des données en RAM et le reste sur le disque avec le mode VM : http://redis.io/topics/virtual-memory. Mais le mode est deprecated.

  • Esprit

    Merci pour cet article très clair ! On entend beaucoup parler de Redis mais c’est la première fois que je vois un article aussi “simple” et compréhensible. (Et quand je dis “simple”, c’est un compliment. Faire simple n’est pas simple. ;-) )

  • moato

    Whoua super intéressant comme article ! Merci qui? Merci Sam&Max ! :D
    Comme je n’avais pas tout de suite compris, je profite de faire une précision sur les opérations atomiques. Ce sont des opérations qui ne sont pas divisible, càd qu’il est garanti que ces opérations soient executées d’un seul bloc. Ainsi, plusieurs threads/processus pouront modifier une ressource via une opération atomique sans qu’il y aie de confilts.

  • Glouton

    Il serait temps que je me mette à utiliser Redis. Ce petit article donne une première approche simplifié et motivante.
    Tout le monde ne jure plus que par ça, notamment dans la gestion du cache étant donné que le clustering est au programme.

    Attention, il y a un vilain double quote qui fout le bordel pour le lien vers l’article sur le HyperLogLog.
    ici

Comments are closed.

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