← Tous les cours / Redis — structures de données et patterns Cours
00 — VUE D'ENSEMBLE

Redis — serveur de structures de données en mémoire

Pas juste un cache. Un serveur monothread qui opère sur des structures natives à des vitesses impossibles pour une base relationnelle.

En PHP, chaque requête HTTP repart à zéro : les variables sont créées, utilisées, puis détruites. Si tu veux partager quelque chose entre deux requêtes — ou entre deux serveurs —, tu dois passer par la base de données.

Redis est une alternative beaucoup plus rapide : c'est un serveur de données en mémoire. Toutes tes applications peuvent lire et écrire dans Redis comme dans une variable partagée. Et parce que tout est en RAM, une lecture prend environ 0,2 millisecondes — contre 5 à 10 ms pour une requête SQL sur SSD.

Redis ne remplace pas ta base SQL
Les données relationnelles, les jointures, les transactions — ça reste dans PostgreSQL. Redis complète : il prend en charge ce qui doit être rapide et partagé. Cache d'objets, sessions, compteurs, files de messages — c'est son terrain.

Redis stocke des clés. Chaque clé peut contenir un type de données différent :

TypeCe que c'estExemple concret
StringUne valeur : texte, nombre, JSON sérialiséCache d'un produit, compteur de vues, token
ListUne séquence ordonnée (file ou pile)File de jobs, feed des 20 dernières activités
HashUn tableau associatif clé → valeurProfil utilisateur, session enrichie
SetUn ensemble de valeurs sans doublonsTags, liste des membres connectés
Sorted SetComme Set, mais chaque valeur a un score numériqueClassement, leaderboard, timeline
💡 Redis traite une commande à la fois. Ça peut sembler une limitation — c'est en réalité une garantie. Un compteur incrémenté par 100 serveurs en même temps ne produira jamais de résultat incorrect, sans que tu écrives un seul lock.

La même requête servie par trois backends. La barre de Redis disparaît presque instantanément — c'est le point.

01 — STRINGS & COMPTEURS

Strings — bien plus que des chaînes de caractères

INCR est atomique par nature. SET NX remplace un mutex. EX remplace un cron de nettoyage.

Un String Redis, c'est une variable partagée entre tous tes serveurs. La valeur peut être du texte, un nombre, un objet JSON sérialisé — n'importe quelle donnée jusqu'à 512 MB.

Les commandes de base

SET    cache:product:42 "{\"name\":\"Widget\",\"price\":9.99}"
GET    cache:product:42    # → le JSON stocké
DEL    cache:product:42    # → supprime la clé
EXISTS cache:product:42    # → 1 (existe) ou 0 (n'existe pas)

Pense à SET comme à une affectation de variable PHP — sauf que cette variable est accessible depuis tous tes serveurs et survit entre toutes tes requêtes.

Compter avec INCR

Imagine que tu veux compter les visites d'une page. En SQL, tu écrirais UPDATE pages SET views = views + 1 WHERE id = 42. En Redis :

INCR views:page:42     # → 1 (la clé est créée à 0 si elle n'existait pas)
INCR views:page:42     # → 2
INCRBY views:page:42 5 # → 7 (ajoute 5 d'un coup)

La différence cruciale : si 50 serveurs lancent INCR simultanément sur la même clé, chaque incrémentation est prise en compte. Aucune n'est perdue. Redis les traite l'une après l'autre — sans que tu écrives un seul verrou.

La durée de vie automatique (TTL)

Tu peux demander à Redis de supprimer une clé automatiquement après un délai. Plus besoin de cron, plus de clés qui s'accumulent :

# Stocker avec expiration directe : EX = secondes
SET cache:product:42 "{...}" EX 300   # expire dans 5 minutes

# Ou ajouter une expiration après coup
SET    session:abc "user:42"
EXPIRE session:abc 1800               # expire dans 30 minutes

# Vérifier le temps restant
TTL session:abc    # → 1742 (secondes restantes)
                   #   -1 si pas de TTL, -2 si la clé n'existe plus
SET avec NX — écriture conditionnelle
SET key value NX n'écrit que si la clé n'existe pas encore. C'est atomique : si deux serveurs tentent la même chose en même temps, un seul réussit. C'est la base d'un verrou distribué simple.

Compteur auto-incrémenté avec TTL. Clique sur +1 vue pour accélérer le compteur. La clé expire et repart à 0.

02 — LISTS

Lists — files, piles et feeds d'activité

O(1) aux extrémités. BLPOP transforme Redis en broker de jobs sans polling.

Imagine la file d'attente à la caisse d'un supermarché. Les clients arrivent et se mettent à la fin (RPUSH). Le caissier prend le premier de la file (LPOP). C'est exactement ce qu'une List Redis modélise : une séquence ordonnée d'éléments que tu alimentes d'un côté et consommes de l'autre.

File et pile

# File FIFO (premier entré, premier sorti) — comme la file au supermarché
RPUSH jobs:email "send:user:42"   # producteur ajoute à droite
RPUSH jobs:email "send:user:99"
LPOP  jobs:email                  # → "send:user:42" — le plus ancien

# Pile LIFO (dernier entré, premier sorti) — comme une pile d'assiettes
LPUSH history "page:A"
LPUSH history "page:B"           # "page:B" est maintenant en tête
LPOP  history                     # → "page:B" — le plus récent
Ajouter ou retirer aux extrémités est instantané
Que ta liste contienne 10 éléments ou 10 millions, LPUSH, RPUSH, LPOP et RPOP prennent toujours le même temps. Par contre, accéder à un élément au milieu de la liste ralentit avec la taille — à éviter.

Lire sans consommer : LRANGE

LRANGE retourne une tranche de la liste sans rien supprimer. C'est idéal pour afficher les N dernières entrées d'un feed :

LRANGE activity:user:42 0 9   # → les 10 premiers éléments (index 0 à 9)
LLEN   activity:user:42       # → nombre total d'éléments

# Garder uniquement les 20 dernières activités (après chaque ajout)
LPUSH activity:user:42 "login"
LTRIM activity:user:42 0 19  # supprime tout au-delà de 20 éléments

BLPOP — attendre qu'un job arrive

Un worker qui vérifie la file toutes les secondes avec LPOP consomme du CPU pour rien et introduit jusqu'à 1 seconde de latence. BLPOP fait mieux : le worker se met en attente, et Redis l'avertit dès qu'un élément arrive.

# Le worker attend indéfiniment (timeout = 0)
# Redis répond immédiatement quand un job est disponible
BLPOP jobs:email jobs:sms 0
# → ["jobs:email", "send:user:42"]

# Ou avec timeout de 30 secondes
BLPOP jobs:email 30
# → nil si rien n'est arrivé en 30 secondes
💡 BLPOP surveille plusieurs files à la fois. BLPOP jobs:email jobs:sms 0 attend sur les deux files en même temps. Le nom de la file est inclus dans la réponse, pour que le worker sache d'où vient le job.

Queue de jobs. Clique sur PUSH pour ajouter, POP pour consommer. Bascule entre mode file (FIFO) et pile (LIFO).

03 — HASHES

Hashes — objets et sessions dans Redis

HSET met à jour un champ sans lire l'objet entier. Encodage compact automatique pour les petits hashes.

En PHP, tu stockerais le profil d'un utilisateur dans un tableau associatif. Un Hash Redis fait la même chose — mais le tableau survit entre toutes tes requêtes et tous tes serveurs peuvent le lire.

// En PHP
$user = ['name' => 'Alice', 'plan' => 'pro', 'logins' => 42];

// En Redis — même structure, même concept
HSET user:42 name "Alice" plan "pro" logins 42

Chaque champ est accessible individuellement :

HGET    user:42 name        # → "Alice"
HMGET   user:42 name plan   # → ["Alice", "pro"]  — plusieurs champs en une commande
HGETALL user:42             # → tous les champs et leurs valeurs
HDEL    user:42 email       # → supprime le champ email
HEXISTS user:42 name        # → 1 (existe) ou 0
HINCRBY user:42 logins 1   # → incrémente logins de 1, sans toucher aux autres champs

Pourquoi pas un String + JSON ?

Tu pourrais stocker un profil entier dans un String avec SET user:42 '{"name":"Alice",...}'. Mais pour mettre à jour le plan de cet utilisateur, tu devrais :

  1. GET user:42 — lire le JSON entier
  2. Désérialiser en PHP
  3. Modifier $user['plan'] = 'enterprise'
  4. Re-sérialiser
  5. SET user:42 '...' — réécrire tout l'objet

Avec un Hash, c'est une seule commande : HSET user:42 plan "enterprise". Redis met à jour ce champ sans toucher aux autres.

Quand utiliser Hash plutôt que String ?
Si tu as un objet dont tu modifies les champs individuellement (profil utilisateur, paramètres, session enrichie) → Hash. Si tu as un objet que tu lis et écris toujours en entier (cache d'un produit que tu n'édites jamais côté Redis) → String avec JSON.
💡 Nommage des clés : Redis n'a pas de namespaces. Par convention, on utilise : comme séparateur — user:42, cache:product:18, session:abc123. Choisis une convention et tiens-t'y dans tout le projet.

Profil utilisateur stocké comme Hash. Clique sur un champ pour voir sa valeur. Ajoute ou supprime des champs.

04 — SETS & SORTED SETS

Sets & Sorted Sets — ensembles et classements

Set pour l'unicité et les opérations ensemblistes. Sorted Set pour les leaderboards, timelines et fenêtres glissantes.

Redis propose deux structures complémentaires basées sur les ensembles. Le Set garantit l'unicité de ses membres. Le Sorted Set ajoute un score flottant à chaque membre et maintient les membres triés en permanence.

Set — ensemble non ordonné
SADD, SREM, SMEMBERS, SISMEMBER. Operations ensemblistes : SUNION (union), SINTER (intersection), SDIFF (différence). Toutes en O(N) sur la taille des sets concernés.
# Membres en ligne (qui est connecté ?)
SADD online:users 42 99 137
SISMEMBER online:users 42   # → 1 (connecté)
SCARD online:users          # → 3 (nombre de membres)

# Tags communs entre deux articles
SINTER tags:article:7 tags:article:12
Sorted Set — ensemble trié par score
Chaque membre a un score flottant. Redis maintient deux index en parallèle : un hashtable (lookup O(1) par membre) et une skiplist (lookup O(log N) par rang ou score). ZADD, ZINCRBY, ZRANK, ZRANGE, ZRANGEBYSCORE, ZREVRANGE.
# Leaderboard
ZADD    leaderboard 1540 "Alice"
ZINCRBY leaderboard 80 "Bob"   # atomic increment du score
ZREVRANGE leaderboard 0 9 WITHSCORES  # top 10
ZRANK   leaderboard "Alice"   # → rang (0-indexed)

# Rate limiting par fenêtre glissante
ZADD ratelimit:user:42 1718107387 "req:unique-id"
ZREMRANGEBYSCORE ratelimit:user:42 0 [now-60s]  # purge les vieilles entrées
ZCARD ratelimit:user:42           # → nombre de requêtes dans la fenêtre

Leaderboard auto-mis à jour avec ZINCRBY. Les scores évoluent en continu — observe les changements de rang.

05 — TTL & ÉVICTION

Expiration et politiques d'éviction

EXPIRE + allkeys-lru = cache LRU automatique. Redis gère le cycle de vie sans cron.

L'expiration est l'une des fonctionnalités les plus utilisées de Redis. Tu associes un TTL (Time To Live) à une clé : Redis la supprime automatiquement à l'échéance. C'est la clé de voûte de tout pattern de cache.

Commandes TTL
EXPIRE key seconds — définit le TTL en secondes. EXPIREAT key timestamp — expire à un timestamp Unix précis. TTL key — retourne les secondes restantes (-1 = pas de TTL, -2 = clé inexistante). PERSIST key — supprime le TTL (rend la clé permanente). PEXPIRE / PTTL — variantes en millisecondes.

Redis gère la suppression des clés expirées de deux façons complémentaires :

Politiques d'éviction (maxmemory-policy)

Quand Redis atteint maxmemory, il applique une politique d'éviction pour libérer de la place :

PolitiqueComportementQuand l'utiliser
noevictionRetourne une erreur si pleinQuand Redis est base de données principale
allkeys-lruÉvince la clé la moins récemment utiliséeCache pur — toutes les clés sont candidates
volatile-lruLRU sur les clés avec TTL seulementMix cache + données permanentes
allkeys-lfuÉvince la clé la moins fréquemment utiliséeCache avec distribution de popularité très inégale
allkeys-randomÉviction aléatoireRarement — distribution uniforme uniquement
🔑 Règle pratique : pour un cache pur, utilise allkeys-lru + maxmemory. Redis se comporte alors exactement comme un cache LRU : tu mets tout ce que tu veux, il supprime automatiquement ce qui n'est plus utilisé. Pas besoin de gérer l'éviction manuellement.

Clés avec TTL en compte à rebours. Les nouvelles clés arrivent, les clés expirées disparaissent. La barre de mémoire montre le remplissage.

06 — PERSISTANCE

Persistance — RDB et AOF

RDB pour les redémarrages rapides. AOF pour minimiser la perte de données. Les deux ensemble en production.

Redis est en mémoire — si le processus redémarre, les données disparaissent. Mais Redis propose deux mécanismes de persistance pour récupérer après un redémarrage : RDB (snapshots) et AOF (journal de commandes).

RDB — Redis Database Backup
Un snapshot complet de la mémoire Redis, écrit sur disque à intervalles configurables. Rapide à charger au démarrage (lecture d'un seul fichier binaire compact). Inconvénient : si Redis crashe entre deux snapshots, toutes les écritures intermédiaires sont perdues.
# redis.conf : snapshot toutes les 15 min si ≥ 1 write
save 900 1
# snapshot toutes les 5 min si ≥ 10 writes
save 300 10
# snapshot toutes les 60 s si ≥ 10 000 writes
save 60 10000

# Déclencher manuellement sans bloquer le serveur
BGSAVE   # fork + snapshot en arrière-plan
AOF — Append Only File
Chaque commande d'écriture est ajoutée à la fin d'un fichier texte. Au redémarrage, Redis rejoue tout le fichier pour reconstruire l'état. La durabilité est configurable : appendfsync always (sûr, lent), appendfsync everysec (recommandé — 1 seconde de perte max), appendfsync no (rapide, pas de garantie).

En production, la combinaison RDB + AOF est recommandée : RDB pour les redémarrages rapides, AOF pour minimiser la perte de données. Redis charge l'AOF au démarrage si les deux existent (plus complet).

Le fichier AOF grossit avec le temps. Redis le compacte automatiquement via AOF rewriting : un processus fork génère un nouvel AOF équivalent mais compact (une seule commande SET au lieu de 1000 INCR successifs).

⚠️ Redis comme cache ? Si tu utilises Redis comme cache pur (les données sont aussi en base SQL), tu peux désactiver toute persistance (save "" dans redis.conf). Redis démarrera avec une mémoire vide — ton application rechargera le cache au fil des requêtes. Overhead zéro, simplicité maximale.

Simulation d'un crash à un moment aléatoire. Bascule entre RDB et AOF pour voir quelle quantité de données est perdue.

07 — PUB/SUB & STREAMS

Pub/Sub & Streams — messagerie Redis

Pub/Sub pour le broadcast temps réel. Streams pour les jobs fiables avec consumer groups et replay.

Redis propose deux mécanismes de messagerie : Pub/Sub pour la diffusion en temps réel sans persistance, et Streams pour un log d'événements durables avec consumer groups.

Pub/Sub — fire and forget
Un publisher envoie un message sur un canal (PUBLISH channel message). Tous les abonnés actifs sur ce canal reçoivent le message instantanément. Si aucun abonné n'est connecté au moment de la publication, le message est perdu. Pas de file d'attente, pas de replay.
# Terminal 1 — abonnement
SUBSCRIBE notifications:user:42

# Terminal 2 — publication
PUBLISH notifications:user:42 '{"type":"order_shipped","id":99}'
# → Terminal 1 reçoit immédiatement le message

# Abonnement par pattern (tous les canaux notifications:*)
PSUBSCRIBE notifications:*
Streams — log persistent avec consumer groups
Un Stream est une structure de données append-only. Chaque entrée a un ID auto-généré (timestamp-séquence). Les consumer groups permettent plusieurs workers de consommer le même stream avec répartition de charge et acknowledgement — comme Kafka, mais intégré à Redis.
# Producteur
XADD events * type "order.placed" order_id 42

# Consommateur simple (lecture depuis le début)
XREAD COUNT 10 STREAMS events 0

# Consumer group (répartition entre workers)
XGROUP CREATE events workers $ MKSTREAM
XREADGROUP GROUP workers worker-1 COUNT 5 STREAMS events >
XACK events workers "1718107387000-0"  # acknowledger un message traité
Pub/SubStreams
PersistanceNonOui
ReplayNonOui
Consumer groupsNonOui
Latence< 1 ms< 5 ms
Quand l'utiliserNotifications temps réel, invalidation de cacheJobs fiables, event sourcing, audit log

Pub/Sub en direct : publie un message sur un canal et vois les abonnés le recevoir instantanément.

08 — PATTERNS AVANCÉS

Patterns avancés — cache, rate limiting, verrous

Cache-aside, rate limiting par fenêtre fixe, verrou distribué SET NX — les patterns Redis en production.

Redis excelle dans des patterns récurrents qu'une base relationnelle résoudrait difficilement : cache à éviction automatique, verrous distribués, rate limiting glissant, sessions partagées entre processus. Ce ne sont pas des hacks — ce sont les cas d'usage pour lesquels Redis a été conçu.

Cache-aside (lazy loading)
L'application vérifie Redis avant d'interroger la base. Si la clé existe (cache hit), retourner directement. Sinon (cache miss), interroger la base, stocker le résultat dans Redis avec un TTL, retourner la valeur. La base n'est jamais sollicitée pour des données récentes.
// PHP — pattern cache-aside
$cached = $redis->get('product:'.$id);
if ($cached !== false) {
    return json_decode($cached);
}
$product = $db->findProduct($id);  // base SQL
$redis->setex('product:'.$id, 300, json_encode($product));
return $product;

Rate limiting

// Fenêtre fixe : max 100 requêtes par minute
$key = 'rl:user:'.$userId.':'.floor(time() / 60);
$count = $redis->incr($key);
if ($count === 1) $redis->expire($key, 60);  // TTL seulement au premier INCR
if ($count > 100) { throw new RateLimitException(); }

Verrou distribué

// SET NX + EX : atomic — l'un ou l'autre, jamais les deux à moitié
$acquired = $redis->set('lock:job:42', $uniqueToken, ['NX', 'EX' => 30]);
if (!$acquired) return;  // un autre worker traite déjà ce job

try {
    // ... traitement du job ...
} finally {
    // Relâcher UNIQUEMENT si c'est notre token (pas un autre worker)
    if ($redis->get('lock:job:42') === $uniqueToken) {
        $redis->del('lock:job:42');
    }
}

Sélectionne un pattern pour voir le flux de commandes Redis et l'état du système.

09 — SYNTHÈSE

Synthèse — choisir la bonne structure

Table de décision, anti-patterns à éviter, intégration PHP et Symfony.

Redis n'est pas une solution universelle. Chaque structure a son domaine de pertinence. Bien choisir, c'est maîtriser Redis.

BesoinStructureCommandes clés
Cache d'objet, session, tokenStringSETEX, GET, SET NX
Compteur atomique, rate limitingStringINCR, INCRBY
File de jobs, feed d'activitéListRPUSH, BLPOP, LRANGE
Objet avec champs modifiablesHashHSET, HGET, HINCRBY
Tags, membership, déduplicationSetSADD, SISMEMBER, SINTER
Classement, timeline, fenêtre glissanteSorted SetZADD, ZINCRBY, ZRANGE
Messagerie durable avec ackStreamXADD, XREADGROUP, XACK
Broadcast temps réel (no replay)Pub/SubPUBLISH, SUBSCRIBE

Anti-patterns à éviter

⚠️ KEYS * en production. KEYS pattern scanne toutes les clés — O(N) sur la base entière. Redis est monothread : pendant ce scan, toutes les autres commandes attendent. Utilise SCAN cursor MATCH pattern COUNT 100 à la place (scan incrémental, non-bloquant).
⚠️ Clés sans TTL dans un cache. Sans TTL, les clés s'accumulent indéfiniment. Soit la mémoire explose, soit Redis applique la politique d'éviction de façon imprévisible. Dans un cache, chaque clé doit avoir un TTL.
⚠️ Stocker des structures énormes dans une seule clé. Un hash avec 500 000 champs ou une liste avec 10 millions d'éléments bloque Redis pendant l'opération qui les parcourt. Partitionner par chunk ou par date.

Redis dans PHP et Symfony

Deux extensions/bibliothèques PHP donnent accès à Redis :

Symfony intègre Redis via deux composants :

# config/packages/framework.yaml
framework:
  cache:
    default_redis_provider: 'redis://localhost'
    pools:
      cache.app:
        adapter: cache.adapter.redis
        default_lifetime: 300
💡 Nommage des clés : adopte une convention stricte dès le début — type:id:champ. Exemples : session:abc123, cache:product:18:v2, ratelimit:user:42:20260611T14. Inclure une version (:v2) facilite l'invalidation globale du cache lors d'un changement de schéma.