Une requête qui prend 200ms sur votre base de données prend toujours 200ms la millionième fois que quelqu’un l’exécute, et chacune de ces exécutions se dispute les mêmes connexions, le même I/O disque et le même CPU. Redis garde la réponse en mémoire, sur un serveur qui ne fait que ça, si bien que la millionième lecture coûte un aller-retour réseau au lieu d’un plan de requête.

Ce que Redis fait réellement comme cache

Redis est un magasin clé-valeur en mémoire. Les valeurs vivent en RAM, donc les lectures et écritures prennent moins d’une milliseconde, contre les dizaines ou centaines de millisecondes que coûte une requête relationnelle dès que des jointures, des index sous pression ou un pool de connexions chargé entrent en jeu. Il se place à côté de votre base de données, pas à sa place : votre application interroge d’abord Redis et ne va à la base de données que si Redis n’a pas la réponse.

Le pattern pour ça s’appelle cache-aside (aussi “lazy loading”), et c’est le premier vers lequel se tournent la plupart des applications :

  1. L’application demande une clé à Redis.
  2. Cache hit — Redis l’a, il la retourne. La base de données ne voit jamais la requête.
  3. Cache miss — Redis ne l’a pas. Il interroge la base de données, écrit le résultat dans Redis, puis le retourne.
1
2
3
4
5
Request → Redis ? ──hit──→ retourne la valeur
            miss
          Base de données → écrit dans Redis → retourne la valeur

La base de données reste la source de vérité. Redis garde une copie rapide et jetable des parties que vous lisez souvent. Si Redis redémarre vide, rien n’est perdu : il se remplit tout seul au prochain lot de cache misses.

Comment lancer Redis avec Docker

Le moyen le plus rapide d’avoir une instance Redis est l’image officielle. Si vous n’avez jamais utilisé Docker, Qu’est-ce que Docker et à quoi ça sert couvre les bases avant d’aller plus loin.

1
docker run --name redis-cache -d -p 6379:6379 redis:8

Cela démarre Redis 8 en arrière-plan, en publiant le port 6379 sur votre machine. Connectez-vous avec redis-cli, installé localement ou lancé depuis un second container sur le même réseau :

1
docker run -it --rm --network container:redis-cache redis:8 redis-cli
1
2
3
4
5
6
127.0.0.1:6379> SET user:42:name "Elena"
OK
127.0.0.1:6379> GET user:42:name
"Elena"
127.0.0.1:6379> TTL user:42:name
(integer) -1

Quand TTL retourne -1, cela signifie que la clé n’a pas d’expiration : elle reste jusqu’à ce que vous la supprimiez ou que Redis l’évince sous la pression mémoire. Pour un cache, vous voulez presque toujours une expiration, pour que les données obsolètes disparaissent d’elles-mêmes.

1
2
3
4
127.0.0.1:6379> SET user:42:name "Elena" EX 300
OK
127.0.0.1:6379> TTL user:42:name
(integer) 297

EX 300 fait expirer la clé dans 300 secondes. PX fait la même chose en millisecondes, et EXPIRE user:42:name 300 pose un TTL sur une clé qui existe déjà.

Pour tout ce qui dépasse une vérification manuelle rapide, exécutez Redis via Compose avec le reste de votre stack — voir Qu’est-ce que Docker Compose et Comment l’Utiliser :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  redis:
    image: redis:8
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --save 60 1 --maxmemory 256mb --maxmemory-policy allkeys-lru

  api:
    build: .
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379

volumes:
  redis-data:

api atteint Redis via le hostname redis parce que Compose place les deux services sur le même réseau défini par l’utilisateur, le mécanisme couvert dans Qu’est-ce qu’un Réseau Docker et Comment l’Utiliser. Le volume redis-data compte moins pour un cache pur que pour une base de données primaire (voir Qu’est-ce qu’un volume Docker et comment l’utiliser) : perdre le cache signifie juste que les lectures suivantes deviennent des cache misses. Gardez-le quand même si un cache chaud après un redémarrage compte pour votre budget de latence.

Comment écrire la logique cache-aside dans votre application

Redis vous donne les commandes ; votre application doit encore les appeler dans le bon ordre. Envelopper une lecture de base de données en cache-aside ressemble à ça :

1
2
3
4
5
6
7
8
9
def get_user(user_id):
    key = f"user:{user_id}"
    cached = redis.get(key)
    if cached is not None:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.set(key, json.dumps(user), ex=300)
    return user

Deux détails font l’essentiel du travail ici :

  • Le TTL (ex=300) n’est pas optionnel. Sans lui, une ligne qui change dans la base de données continue de servir son ancienne valeur depuis Redis pour toujours. Réglez le TTL sur la durée pendant laquelle une donnée obsolète est acceptable pour cette clé précise, pas sur un chiffre global pour toute l’application.
  • json.dumps/json.loads. Redis stocke des chaînes, plus une poignée de types plus riches comme les hashes et les sorted sets — jamais les objets de votre langage. Tout ce qui est structuré doit être sérialisé à l’entrée et parsé à la sortie.

Quand la ligne sous-jacente change, supprimez la clé au lieu d’attendre le TTL :

1
2
3
def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")

La lecture suivant une mise à jour est un cache miss garanti, qui repeuple Redis avec la ligne fraîche. Cache-aside n’essaie jamais de garder le cache synchronisé en temps réel. Il fait disparaître les entrées obsolètes rapidement, par TTL ou par suppression explicite à l’écriture.

Éviction : que se passe-t-il quand Redis manque de mémoire

Redis garde tout en RAM, donc la mémoire est finie d’une façon dont le disque ne l’est généralement pas. Fixez un plafond dur avec maxmemory, et dites à Redis quoi faire une fois ce plafond atteint avec maxmemory-policy :

PolitiqueComportementÀ utiliser pour
noevictionRefuse les nouvelles écritures une fois plein, retourne une erreurUn datastore qui ne peut pas perdre de données silencieusement
allkeys-lruÉvince la clé la moins récemment utilisée, n’importe laquelleUn cache pur — le choix par défaut le plus judicieux
volatile-lruÉvince la clé la moins récemment utilisée parmi celles avec un TTLMélanger clés de cache et clés permanentes dans une instance
volatile-ttlÉvince en premier la clé avec le TTL restant le plus courtRate limiters, tokens de courte durée

Pour une instance de cache dédiée, allkeys-lru est presque toujours le bon choix : chaque clé y est jetable par définition, alors laissez Redis jeter ce que vous avez touché le moins récemment. noeviction est la mauvaise politique pour un cache, parce qu’elle transforme “Redis est plein” en erreurs applicatives au lieu de laisser tomber silencieusement les entrées froides.

1
2
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru

Réglez les deux dans la commande de démarrage, comme dans le fichier Compose ci-dessus, pas seulement au runtime. Un redémarrage du container remet à zéro un CONFIG SET posé seulement au runtime, ramenant Redis à ses valeurs par défaut (maxmemory 0, c’est-à-dire illimité, plafonné seulement par l’hôte).

Quand le cache Redis est le mauvais choix

Le cache masque un chemin lent ; il ne le répare pas. Quatre situations où recourir à Redis en premier aggrave les choses :

  • Les données changent à chaque lecture de toute façon. Un ticker boursier en direct ou un compteur incrémenté à chaque requête ne gagne rien d’un cache invalidé aussi vite qu’il est peuplé. Vous avez ajouté un aller-retour réseau sans gagner en hit rate.
  • Vous avez besoin d’une cohérence forte. Cache-aside est eventually consistent par conception : entre une écriture et le cache miss suivant, il existe une fenêtre où une valeur obsolète peut encore être servie. Pour un solde de compte ou un niveau de stock où cette fenêtre cause un vrai dommage, ne le mettez pas en cache, ou mettez-le en cache avec un TTL de quelques secondes et acceptez le compromis explicitement.
  • Le vrai goulot d’étranglement est ailleurs. Si votre base de données est lente à cause d’un index manquant ou d’une requête N+1, réglez ça d’abord. Mettre en cache une requête mal écrite déplace juste la même mauvaise réponse en mémoire, plus vite.
  • Un thundering herd à l’expiration. Quand une clé chaude expire, toutes les requêtes concurrentes peuvent manquer le cache en même temps et marteler la base de données simultanément. Utilisez SET key value NX EX 30 comme verrou de courte durée, détenu par la requête qui repeuple le cache, pour que les autres attendent celle-là au lieu de s’entasser sur la base de données.

Par où commencer avec le cache Redis

Placez un container redis:8 devant votre lecture la plus lente et la plus répétée. Enveloppez-la en cache-aside avec un TTL que vous pouvez justifier, pas un chiffre rond choisi au hasard, et réglez maxmemory-policy allkeys-lru pour qu’un cache plein dégrade au lieu d’échouer. Ensuite, mesurez le hit rate. Le write-through caching, l’invalidation par pub/sub et Redis Streams coûtent tous en complexité, et aucun ne vaut le coup avant qu’un simple cache-aside vous ait montré précisément où il atteint ses limites.

Articles associés