Redis, c'est de la bombe

Je vous parlais en décembre avec grande admiration, de Neo4j. J'ai finalement du laisser tomber cette base de données pour mon projet pour des raisons de performance. En effet, c'est quand même assez lourd quand on commence à avoir beaucoup de données.
Entre temps, NodeBB est sorti. Ce forum écrit en NodeJs utilise une base de données NOSQL encore différente : Redis. Cette base de données à la particuliarité d'être un K/V Store, un stockage de clés/valeurs. C'est à dire que c'est encore quelque chose de très différent des bases relationnelles (PGSql, MySQL…), des bases NOSQL orientées document (MongoDB, CouchDB) ou des bases NOSQL orientées graph (Neo4J).

Disclaimer : Je m'excuse à l'avance quant à l'éventuel manque de précision de ce post : je ne suis pas DBA mais webdev ;)

Des clés et des valeurs

Ici, pas question de tables, ni même de collections d'objets complexes et encore moins de relations. On va stocker une valeur pour une clé. Heureusement une clé peut être un ensemble hiérarchique de groupes de clés. On peut ainsi arriver à stocker un groupe de clés/valeurs dans un même sous ensemble. Par exemple, nous pouvons stocker la valeur bar pour la clé foo. Mais aussi une autre valeur pour la clé foo:1, puis foo:2, etc. On peut de la sorte mimer une table. La "table" foo matérialisée par la clé foo:{id}, avec des entrées foo:1, foo:2, foo:3, etc. Pour chacun de ces groupes de clés, on peut stocker d'autres paires de clés/valeurs. On peut alors stocker des utilisateurs de la sorte :

user:1 => {
  username: "mauricemoss",
  firstname: "Maurice",
  lastname: "Moss"
}
user:2 => {
  username: "roytrenneman",
  firstname: "Roy",
  lastname: "Trenneman"
}

Tout d'abord, il faut construire un index des uid sous le groupe global :

HINCRBY global nextUID 1

qui retournera la nouvelle valeur de nextUID et qui deviendra l'uid suivant.
On peut alors insérer nos données :

> HSET user:1 username "mauricemoss"
"1"
> HSET user:1 firstname "Maurice"
> HSET user:1 lastname "Moss"

Et le second user :

> HINCRBY global nextUID 1
"2"
> HSET user:2 username "roytrenneman"
> HSET user:2 firstname "Roy"
> HSET user:2 lastname "Trenneman"

Pour retrouver les valeurs du premier utilisateur, il suffit de demander les valeurs de la clé user:1 :

> HGETALL user:1
1) "username"
2) "mauricemoss"
3) "firstname"
4) "Maurice"
5) "lastname"
6) "Moss"

> HGETALL user:2
1) "username"
2) "roytrenneman"
3) "firstname"
4) "Roy"
5) "lastname"
6) "Trenneman"

Maintenant, on voudrait pouvoir récupérer un utilisateur à partir de l'une de ses valeurs. Par exemple, retrouver un utilisateur à partir de son username. Avec la plupart des autres bases de données, c'est très facile. Ici, ça demande un peu de gymnastique car rien n'est prévu par défaut pour indexer ce genre de choses automatiquement. Il faut donc générer son index manuellement lors des insertion de données. Nous allons créer un groupe de clés username:uid dans lequel nous allons stocker les associations entre le username et l'UID. On pourra alors facilement y retrouver un uid (la valeur) à partir d'un username (la clé) :

> HSET username:uid mauricemoss 1
> HSET username:uid roytrenneman 2

Je pourrais alors simplement demander quel est l'UID du username mauricemoss :

> HGET username:uid mauricemoss
"1"

Et je peux utiliser cette réponse pour créer la clé correspondant à l'utilisateur :

> HGETALL user:1

C'est donc une façon de structurer ses données vraiment très différente de ce que l'on a l'habitude de voir. Ça demande pas mal de réflexion et de structuration. C'est un exercice particulièrement intéressant. Et le gros avantage se situe au niveau des performances.

Performances

Comme memcache, cette base de données stocke toutes ses données en RAM. Elles sont répliquées sur le disque régulièrement selon la configuration de la base. Et donc l'accès aux données ne peut être plus rapide et met une claque à la concurrence.
Je ne suis pas suffisement compétent dans le domaine pour parler avec précision de benchmarks de base de données, mais j'ai fait un petit test très simple : j'ai inséré 10 000 groupes de clés/valeurs, l'équivalent d'une table user de 10000 entrées.

Cette insertion est un peu longue : 2 secondes pour 1000 insertions. Chaque insertion suit le même processus qu'expliqué dans le chapitre précedent : incrémentation de la clé global/nextUid, insertion dans le groupe username:uid et insertion des données dans user:{uid}. Il aura donc fallu 20 secondes pour insérer ces 10 000 users.

J'ai ensuite récupéré les données complètes de ces 10 000 utilisateurs. Une première requête dans username:uid pour récolter un tableau des 10 000 uid, puis une requête pour chaque uid (donc 10 000 requêtes) pour récupérer les données dans le groupe user:{uid}. Je récupère 1,3Mo de données en seulement 2,2 seconde (sur une VM sur mon Macbook Pro Retina).

Ça veut dire qu'avec la majorité des applications courante, on peut facilement laisser l'applicatif gérer le tri et la selection des données à coups de map/reduce sans se soucier de lenteurs.

C'est de la bombe de base ! C'est ma nouvelle amie.

Hadrien

Hi, I'm a french Web Lead Developer, Front End Architect from Toulouse, France. I've worked for 7 years for Overblog then 2 years with AngularJS. Now, I'm a great fan of Aurelia.io.

Toulouse, France https://hadrien.eu