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.
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 :
| Type | Ce que c'est | Exemple concret |
|---|---|---|
| String | Une valeur : texte, nombre, JSON sérialisé | Cache d'un produit, compteur de vues, token |
| List | Une séquence ordonnée (file ou pile) | File de jobs, feed des 20 dernières activités |
| Hash | Un tableau associatif clé → valeur | Profil utilisateur, session enrichie |
| Set | Un ensemble de valeurs sans doublons | Tags, liste des membres connectés |
| Sorted Set | Comme Set, mais chaque valeur a un score numérique | Classement, leaderboard, timeline |
La même requête servie par trois backends. La barre de Redis disparaît presque instantanément — c'est le point.
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 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.
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
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 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).
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 :
GET user:42— lire le JSON entier- Désérialiser en PHP
- Modifier
$user['plan'] = 'enterprise' - Re-sérialiser
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.
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.
: 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.
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.
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
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.
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.
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 :
- Lazy deletion : une clé expirée n'est supprimée physiquement qu'au prochain accès (
GET,EXISTS…). Coût zéro en dehors des accès. - Active expiration : Redis passe en revue un sous-ensemble de clés avec TTL toutes les 100 ms et supprime celles qui ont expiré. Évite l'accumulation de clés "fantômes".
Politiques d'éviction (maxmemory-policy)
Quand Redis atteint maxmemory, il applique une politique d'éviction pour libérer de la place :
| Politique | Comportement | Quand l'utiliser |
|---|---|---|
noeviction | Retourne une erreur si plein | Quand Redis est base de données principale |
allkeys-lru | Évince la clé la moins récemment utilisée | Cache pur — toutes les clés sont candidates |
volatile-lru | LRU sur les clés avec TTL seulement | Mix cache + données permanentes |
allkeys-lfu | Évince la clé la moins fréquemment utilisée | Cache avec distribution de popularité très inégale |
allkeys-random | Éviction aléatoire | Rarement — distribution uniforme uniquement |
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.
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).
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
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).
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.
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.
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:*
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/Sub | Streams | |
|---|---|---|
| Persistance | Non | Oui |
| Replay | Non | Oui |
| Consumer groups | Non | Oui |
| Latence | < 1 ms | < 5 ms |
| Quand l'utiliser | Notifications temps réel, invalidation de cache | Jobs fiables, event sourcing, audit log |
Pub/Sub en direct : publie un message sur un canal et vois les abonnés le recevoir instantanément.
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.
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.
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.
| Besoin | Structure | Commandes clés |
|---|---|---|
| Cache d'objet, session, token | String | SETEX, GET, SET NX |
| Compteur atomique, rate limiting | String | INCR, INCRBY |
| File de jobs, feed d'activité | List | RPUSH, BLPOP, LRANGE |
| Objet avec champs modifiables | Hash | HSET, HGET, HINCRBY |
| Tags, membership, déduplication | Set | SADD, SISMEMBER, SINTER |
| Classement, timeline, fenêtre glissante | Sorted Set | ZADD, ZINCRBY, ZRANGE |
| Messagerie durable avec ack | Stream | XADD, XREADGROUP, XACK |
| Broadcast temps réel (no replay) | Pub/Sub | PUBLISH, SUBSCRIBE |
Anti-patterns à éviter
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).
Redis dans PHP et Symfony
Deux extensions/bibliothèques PHP donnent accès à Redis :
- phpredis : extension C, API proche des commandes Redis natives, performante.
$redis->set('key', 'val'). - Predis : bibliothèque PHP pure, aucune extension à installer. Idéale pour les environnements contraints.
Symfony intègre Redis via deux composants :
- Symfony Cache : adapters Redis pour
CacheItemPoolInterface(PSR-6). Configurecache.adapter.redisdansframework.yaml. - Symfony HttpFoundation :
RedisSessionHandlerpour stocker les sessions Symfony dans Redis.
# config/packages/framework.yaml
framework:
cache:
default_redis_provider: 'redis://localhost'
pools:
cache.app:
adapter: cache.adapter.redis
default_lifetime: 300
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.