Linux Embedded

Le blog des technologies libres et embarquées

Introduction à Redis: une base de données "in-memory" clé-valeur

1. Qu'est-ce que Redis ?

Redis est une technologie sous license BSD qui est notamment utilisée en tant que cache, agent de message (broker) et pour l'enregistrement de structures complexes pouvant persister sur disque. La première release a été publiée en 2009 par Salvatore Sanfilippo. Avec plusieurs centaines de millions d'opérations par seconde effectués par l'ensemble des utilisateurs, Redis vient concurrencer le haut du classement des bases de données en situant à la 8e position aujourd'hui, selon le classement de "db-engines rankings".

Dans cet article, nous allons nous baser sur la version la plus récente aujourd'hui : 6.0.8.

Avantages majeurs

  • Souplesse : Redis est supporté par de nombreux langages de programmation (C, C#, C++, Java, Python, Perl, PHP, Node.js...).
  • Rapidité : Sa latence est maintenue inférieure à la milliseconde.
  • Maintenabilité : Les releases sont assez fréquentes, et de plus en plus de nouvelles fonctionnalités sont mises en place.
  • Simplicité : Aussi bien dans la mise en place de l'environnement que dans son utilisation, la prise en main est quasi-immédiate.

Persistence

Par défaut, Redis sert de cache. Si l'utilisateur souhaite rendre ses données persistantes, deux possibilités s'offrent alors à lui : RDB et AOF. S'il le souhaite, l'utilisateur peut également utiliser les deux moyens de persistance sur une même instance.

RDB (Redis DataBase file): C'est en réalité un seul fichier compact qui est un snapshot de l'ensemble des données (dataset) sur un intervalle de temps spécifié. Le principal avantage du RDB est la possibilité de sauvegarder de manière régulière ses données, pour restaurer facilement différents enregistrements sur des durées déterminées.
AOF (Append-Only File): C'est un fichier qui décrit de manière précise la pile d'opérations effectuées sur le serveur. Comparé à son alternative, il sera moins sensible aux problèmes de corruptions. En revanche, sa taille est bien plus importante.

 

Dans cet article, nous allons d'abord voir comment mettre en place le serveur Redis, ainsi qu'un client utilitaire. Ensuite, nous verrons l'aspect bus / cache de Redis, présenterons plusieurs types, et enfin introduirons le scripting LUA.

2 Mise en place de l'environnement Redis

2.1 Serveur redis

2.1.1 Installation classique

$ wget http://download.redis.io/releases/redis-6.0.8.tar.gz
$ tar xzf redis-6.0.8.tar.gz
$ cd redis-6.0.8
$ make
$ src/redis-server &

Le package de release inclut l'outil ligne de commande pour directement effectuer les requêtes sur le serveur. Le binaire "redis-cli" est localisé dans le répertoire src/.

$ src/redis-cli -p 6379

2.1.2 Avec Docker

$ docker run -d --name my-redis redis:6.0.8

Cette commande vous permet de lancer redis-server en background. Pour avoir accès à la ligne de commande, vous pouvez tout simplement exécuter redis-cli dans le container crée :

$ docker exec -it my-redis redis-cli

2.1.3 Intégration depuis Yocto

Openembedded fournit déjà la recette Redis contenant le serveur et la CLI (sous meta-openembedded/meta-oe/recipes-extended/redis_<version>.bb).

Il suffit de rajouter dans votre recette d'image la ligne suivante :

IMAGE_INSTALL_append = " redis"

2.2 Un utilitaire client très user-friendly : redis-commander

redis-commander permet d'effectuer toutes les opérations possibles  en ligne de commande, mais depuis une page web.

Redis-Commander aperçu

2.2.1 Installation classique

$ npm install -g redis-commander
$ redis-commander &

2.2.2 Avec Docker

Que vous ayez installé Redis sur une machine distante ou non, vous pouvez utiliser docker pour redis-commander.

docker-compose.yml:

version: "3.7"

services:

  redis:
    image: redis:6.0.8
    container_name: my-redis
    ports:
      - 6379:6379
    command: ["redis-server", "--appendonly", "yes"]

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - 8081:8081
    depends_on:
      - redis
$ docker-compose up

Ou si redis-server ne tourne pas dans un container :

$ docker run --rm --name redis-commander -d \
  --env REDIS_HOSTS=local:localhost:6379 \
  -p 8081:8081 \
  rediscommander/redis-commander:latest

3 Redis en tant que broker

Nous allons discuter dans un premier temps de l'utilisation (la moins fréquente) de Redis en tant qu'agent de message. Cette utilisation peut notamment servir de bus applicatif pour la communication inter-processus. Nous avons brièvement précisé que de nombreux langages de programmation possédaient une API Redis; il est alors possible de faire de l'IPC avec le bus applicatif fourni par Redis. Il fonctionne comme un système classique Pub/Sub :

En exécutant deux clients (avec redis-commander ou avec la CLI) connectés à la même instance redis-server, on peut immédiatement comprendre le fonctionnement. Un premier "clientA" va souscrire au topic "/Topic1/SousTopic1", et un second client "clientB" va publier un message sur ce même topic.

clientA:

127.0.0.1:6379> SUBSCRIBE /Topic1/SousTopic1
1) "subscribe"
2) "/Topic1/SousTopic1"
3) (integer) 1

clientB:

127.0.0.1:6379> PUBLISH /Topic1/SousTopic1 "Hello www.linuxembedded.fr!"
(integer) 1


Output clientA:

Reading messages... (press Ctrl-C to quit)
1) "message"
2) "/Topic1/SousTopic1"
3) "Hello www.linuxembedded.fr!"

Pour l'opération "subscribe" (la casse n'est pas prise en compte dans Redis), à chaque réception de message sur le topic, les APIs auront différentes informations dans leur callback. Un trigger (subscribe, unsubscribe, message), le topic en question, et si le trigger est un message, la charge utile de celui-ci.
Il est possible d'utiliser des wildcards (*) pour la souscription; il suffit pour cela d'utiliser la commande PSUBSCRIBE à la place. Le callback fournira alors une information supplémentaire lors de la réception d'un "pmessage": Le topic général souscrit :

127.0.0.1:6379> PSUBSCRIBE /*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "/*"
3) (integer) 1
1) "pmessage"
2) "/*"
3) "/Topic1/SousTopic1"
4) "Hello www.linuxembedded.fr!"

Exemple de l'implémentation d'un code minimal en C avec callback en utilisant la librairie "hiredis" et "libevent" :

#include <stdio.h>
#include <hiredis.h>
#include <string.h>
#include <async.h>
#include <adapters/libevent.h>

static void handle_subscribe(redisAsyncContext *c, void *reply, void *userContext)
{
        redisReply *r = reply;
        if (reply == NULL) return;

        if (r->type == REDIS_REPLY_ARRAY) {
            for (int j = 0; j < r->elements; j++) {
                printf("%u) %s\n", j, r->element[j]->str);
            }
        }
}

int main(int argc, char **argv)
{
        struct event_base *base = event_base_new();
        redisAsyncContext *r_ctx = redisAsyncConnect("127.0.0.1", 6379);
        if (r_ctx == NULL||r_ctx->err)
        {
                fprintf(stderr, "Redis error");
                return 1;
        }
        redisLibeventAttach(r_ctx, base);
        redisAsyncCommand(r_ctx, handle_subscribe, NULL, "subscribe TOPIC1");
        event_base_dispatch(base);
        return 0;
}

Ce code est à compiler avec les options -lhiredis et -levent.

4 Redis en tant que base de données

4.1 Types basiques

Redis est une base de données de la forme clef-valeur, mais chaque valeur peut contenir une structure de données plus ou moins complexe (chaîne de caractères, liste, hash). Par habitude, nous organisons les clefs de cette manière : "aaa:bbb:ccc"; avec un utilitaire du style redis-commander, les clefs seront alors organisées.

Commençons par utiliser des simples opérations GET/SET :

127.0.0.1:6379> SET compositeurs:beethoven:age 250
OK
127.0.0.1:6379> GET compositeurs:beethoven:age
"250"
127.0.0.1:6379> EXISTS compositeurs:beethoven:age
(integer) 1
127.0.0.1:6379> INCR compositeurs:beethoven:age
(integer) 251
127.0.0.1:6379> DECR compositeurs:beethoven:age
(integer) 250
127.0.0.1:6379> DEL compositeurs:beethoven:age
(integer) 1
127.0.0.1:6379> EXISTS compositeurs:beethoven:age
(integer) 0

A présent, quelques exemples avec une liste de quelques symphonies célèbres de Beethoven :

127.0.0.1:6379> RPUSH compositeurs:beethoven:symphonies Eroica
(integer) 1
127.0.0.1:6379> RPUSH compositeurs:beethoven:symphonies Pastoral Choral
(integer) 3
127.0.0.1:6379> LRANGE compositeurs:beethoven:symphonies 0 -1
1) "Eroica"
2) "Pastoral"
3) "Choral"
127.0.0.1:6379> RPUSH compositeurs:beethoven:symphonies ErrorValue
(integer) 4
127.0.0.1:6379> RPOP compositeurs:beethoven:symphonies
"ErrorValue"
127.0.0.1:6379> LRANGE compositeurs:beethoven:symphonies 0 -1
1) "Eroica"
2) "Pastoral"
3) "Choral"

Enfin, un exemple de type important : le hash. Les hash sont très utilisés pour la représentation des objets; utilisons les pour associer le numéro des symphonies avec leur nom :

127.0.0.1:6379> DEL compositeurs:beethoven:symphonies
(integer) 1
127.0.0.1:6379> HMSET compositeurs:beethoven:symphonies:3 name "Eroica" date "1803"
OK
127.0.0.1:6379> HMSET compositeurs:beethoven:symphonies:6 name "Pastoral" date "1808"
OK
127.0.0.1:6379> HMSET compositeurs:beethoven:symphonies:9 name "Choral" date "1822"
OK
127.0.0.1:6379> HGETALL compositeurs:beethoven:symphonies:9
1) "name"
2) "Choral"
3) "date"
4) "1822"
127.0.0.1:6379> HGET compositeurs:beethoven:symphonies:9 date
"1822"

Voici ce que l'on peut voir sur redis-commander:

Aperçu sur redis-commander

Je vous renvoie à la documentation Redis pour l'ensemble des commandes moins fréquemment utilisées.

4.2 Notifications keyspace

Un client peut s'abonner sur des modifications de clefs, ou sur de manière générale sur des opérations de la même manière qu'il s'abonnerait à un topic Pub/Sub. Il y a un prérequis pour cela : il faut ajouter une configuration dans le serveur Redis (non présente par défaut dû à la consommation CPU) :

127.0.0.1:6379> CONFIG SET notify-keyspace-events AKE

Il est également possible de configurer le serveur directement dans le fichier redis.conf (sans oublier de redémarrer le service !).

Voici la syntaxe de base : <PUBSUB OPERATION> __keyspace@<NumeroDb>__:<Clef/RedisOperation> [<argument>]

L'exemple le plus classique et sûrement la plus utilisée est la souscription de modification de clef :

clientA:

127.0.0.1:6379> PSUBSCRIBE __keyspace@0__:compositeurs:mozart:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyspace@0__:compositeurs:mozart:*"
3) (integer) 1

clientB:

127.0.0.1:6379> set compositeurs:mozart:nationalite autrichien
OK

Output clientA:

1) "pmessage"
2) "__keyspace@0__:compositeurs:mozart:*"
3) "__keyspace@0__:compositeurs:mozart:nationalite"
4) "set"

On remarque en effet que la valeur de la clef modifiée n'est pas présente dans l'output de clientA : celui-ci est notifié d'un changement, et s'il souhaite récupérer la nouvelle valeur, il doit de nouveau aller la chercher dans la base.

Pour plus de documentation : https://redis.io/topics/notifications

5 LUA scripting

Redis possède un interpréteur qui est capable d' exécuter des scripts LUA. A titre d'exemple simple, commençons par créer un fichier "myscript.lua" :

myscript.lua:

local value = "Hello from LUA."
return value

Nous pouvons ainsi charger ce fichier à partir de la CLI de redis.

$ redis-cli --eval myscript.lua
"Hello from LUA."

A présent, améliorons notre script afin qu'il retourne la valeur d'une clef donnée en paramètre, et "nil" si elle n'existe pas.

myscript.lua:

if redis.call("EXISTS", KEYS[1]) == 1 then
  local payload = redis.call("GET", KEYS[1])
  return payload
else
  return nil
end

"redis.call()" est une fonction qui permet d'envoyer des commandes Redis.

$ redis-cli set test:key testvalue
$ redis-cli --eval myscript.lua test:key
"testvalue"

Il est également possible d'exécuter un script à partir d'une commande redis avec l'opération EVAL :

127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 clef1 clef2 variable1 variable2
1) "clef1"
2) "clef2"
3) "variable1"
4) "variable2"

Le deuxième argument de l'opération EVAL est le nombre de clefs passées en paramètre. Celles-ci seront donc récupérées facilement via la variable KEYS : KEYS[n] est le nième paramètre. ARGV est quant à elle la variable contenant les arguments supplémentaires, dans l'exemple ici, "variable1" et "variable2".

6 Conclusion

Avec une utilisation croissante au fil des années, Redis est devenu l'une des bases de données "in-memory" les plus populaires. J'ai eu moi-même l'occasion d'utiliser deux serveurs Redis dont un serveur de configuration qui persistait des données, et l'autre qui servait de cache et de bus pour de l'IPC sur une cible iMX7.

Cet article était une brève introduction, et il existe bien évidemment d'autres features de Redis n'ont pas été présentées (modules, pipelines, partionning); je vous invite cependant à découvrir au travers de la documentation officielle toutes les solutions proposées.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.