Redis est bien plus qu'un simple cache en mémoire. Cet article détaille les patterns de caching avancés, les stratégies d'éviction, la mise en place d'un cluster Redis et les bonnes pratiques pour des performances optimales en production.
Plan
- Architecture Redis et cas d'usage
- Patterns de caching essentiels
- Éviction policies et configuration mémoire
- Redis Cluster : haute disponibilité et scalabilité
- Persistance : RDB vs AOF
- Monitoring et optimisation
- Sécurité Redis
- Conclusion
Architecture Redis et cas d'usage
Pourquoi Redis pour le cache ?
Avantages :
- ⚡ Ultra-rapide : opérations en mémoire, latence sub-milliseconde
- 📊 Structures de données riches : strings, hashes, lists, sets, sorted sets, streams
- 🔄 Éviction automatique : gestion intelligente de la mémoire
- 📡 Pub/Sub intégré : messaging temps réel
- 🚀 Clustering natif : scalabilité horizontale
- 💾 Persistance optionnelle : RDB snapshots ou AOF
Cas d'usage typiques :
- Cache de sessions utilisateur
- Cache de résultats de requêtes SQL
- Rate limiting / throttling
- Leaderboards / classements
- Real-time analytics
- Job queues
- Cache de pages HTML
Installation
# Debian/Ubuntu
apt install redis-server
# RHEL/Rocky
dnf install redis
# Depuis source (dernière version)
wget https://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
make install
# Vérifier
redis-cli ping
# PONG
Patterns de caching essentiels
1. Cache-Aside (Lazy Loading)
Pattern le plus courant : l'application gère le cache manuellement.
import redis
import psycopg2
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
db = psycopg2.connect("dbname=mydb")
def get_user(user_id):
# 1. Essayer cache
cache_key = f"user:{user_id}"
user = r.get(cache_key)
if user:
print("Cache HIT")
return json.loads(user)
# 2. Cache MISS : requête DB
print("Cache MISS")
cursor = db.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
# 3. Mettre en cache (TTL 1h)
if user:
r.setex(cache_key, 3600, json.dumps(user))
return user
def update_user(user_id, data):
# Mise à jour DB
cursor = db.cursor()
cursor.execute("UPDATE users SET name = %s WHERE id = %s", (data['name'], user_id))
db.commit()
# Invalider cache
r.delete(f"user:{user_id}")
Avantages :
- Simple à implémenter
- Cache uniquement ce qui est utilisé
- Résilient (cache down = app continue)
Inconvénients :
- Cache MISS = 2 roundtrips (check cache + DB)
- Risque de thundering herd
2. Write-Through
L'application écrit simultanément en cache et en DB.
def update_user(user_id, data):
# 1. Écrire en DB
cursor = db.cursor()
cursor.execute("UPDATE users SET name = %s WHERE id = %s", (data['name'], user_id))
db.commit()
# 2. Écrire en cache
cache_key = f"user:{user_id}"
r.setex(cache_key, 3600, json.dumps(data))
Avantages :
- Cache toujours à jour
- Pas de cache MISS après écriture
Inconvénients :
- Écritures plus lentes (2 opérations)
- Cache peut contenir données jamais lues
3. Write-Behind (Write-Back)
L'application écrit en cache, Redis écrit en DB de façon asynchrone.
# Écriture immédiate en cache
def update_user(user_id, data):
cache_key = f"user:{user_id}"
r.setex(cache_key, 3600, json.dumps(data))
# Ajouter à queue de sync DB
r.lpush("db_sync_queue", json.dumps({
'user_id': user_id,
'data': data
}))
# Worker async pour sync DB
def db_sync_worker():
while True:
item = r.brpop("db_sync_queue", timeout=5)
if item:
data = json.loads(item[1])
# Écrire en DB
cursor = db.cursor()
cursor.execute("UPDATE users SET name = %s WHERE id = %s",
(data['data']['name'], data['user_id']))
db.commit()
Avantages :
- Écritures ultra-rapides
- Réduit charge DB
Inconvénients :
- Complexe à implémenter
- Risque perte de données si Redis crash
4. Read-Through
Redis charge automatiquement depuis la DB si cache MISS.
Nécessite un proxy ou middleware (ex: Redis with RedisGears).
# Pseudo-code avec RedisGears
def read_through_loader(key):
user_id = key.split(':')[1]
cursor = db.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
return json.dumps(user) if user else None
# Enregistrer fonction
rg.GearsBuilder().map(read_through_loader).register('user:*')
5. Cache de résultats de requêtes complexes
import hashlib
def get_products_by_category(category, filters):
# Hash de la requête pour clé cache
query_hash = hashlib.md5(
f"{category}:{json.dumps(filters)}".encode()
).hexdigest()
cache_key = f"products:query:{query_hash}"
# Essayer cache
result = r.get(cache_key)
if result:
return json.loads(result)
# Requête complexe
cursor = db.cursor()
cursor.execute("""
SELECT p.* FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name = %s
AND p.price BETWEEN %s AND %s
AND p.in_stock = true
ORDER BY p.popularity DESC
LIMIT 50
""", (category, filters['min_price'], filters['max_price']))
products = cursor.fetchall()
# Cache 5 minutes
r.setex(cache_key, 300, json.dumps(products))
return products
Éviction policies et configuration mémoire
Stratégies d'éviction
Quand Redis atteint maxmemory, il doit évict des clés selon la policy.
# /etc/redis/redis.conf
# Limite mémoire (75% de RAM disponible recommandé)
maxmemory 4gb
# Policy d'éviction
maxmemory-policy allkeys-lru
Policies disponibles :
| Policy | Description | Cas d'usage |
| noeviction | Retourne erreur si mémoire pleine | Cache strict, jamais perdre données |
| allkeys-lru | Évict clés LRU (Least Recently Used) globalement | Cache général (recommandé) |
| allkeys-lfu | Évict clés LFU (Least Frequently Used) | Cache avec patterns accès inégaux |
| volatile-lru | Évict clés LRU avec TTL uniquement | Mix cache + données persistantes |
| volatile-lfu | Évict clés LFU avec TTL uniquement | Idem avec fréquence |
| allkeys-random | Évict clés aléatoirement | Cache uniforme, peu de logique |
| volatile-random | Évict clés avec TTL aléatoirement | Rarement utile |
| volatile-ttl | Évict clés avec TTL le plus court | Prioriser clés fraîches |
Recommandations :
- Cache pur :
allkeys-lru(par défaut pour cache) - Sessions + cache :
volatile-lru(sessions sans TTL restent) - Patterns non-uniformes :
allkeys-lfu
Configuration mémoire optimale
# redis.conf
# === MÉMOIRE ===
maxmemory 4gb
maxmemory-policy allkeys-lru
# Samples pour LRU (plus élevé = plus précis mais plus lent)
maxmemory-samples 5
# === PERFORMANCE ===
# Désactiver THP (Transparent Huge Pages)
# Mettre dans /etc/rc.local :
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
# TCP backlog
tcp-backlog 511
# Timeout connexions inactives (0 = désactivé)
timeout 300
# TCP keepalive
tcp-keepalive 300
# === PERSISTANCE (si nécessaire) ===
# RDB : snapshots
save 900 1 # Après 900s si >= 1 changement
save 300 10 # Après 300s si >= 10 changements
save 60 10000 # Après 60s si >= 10000 changements
# AOF : append-only file (plus sûr)
appendonly no # Activer si besoin persistance
appendfsync everysec
# === OPTIMISATIONS ===
# Lazy freeing (libère mémoire en background)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
# Compression (économise mémoire pour grandes valeurs)
list-compress-depth 1
TTL et expiration
# TTL simple (secondes)
r.setex("key", 3600, "value") # Expire dans 1h
# TTL avec EXPIRE
r.set("key", "value")
r.expire("key", 3600)
# TTL timestamp Unix
import time
expire_at = int(time.time()) + 3600
r.expireat("key", expire_at)
# Vérifier TTL restant
ttl = r.ttl("key")
# -1 = pas de TTL
# -2 = clé n'existe pas
# >0 = secondes restantes
# Supprimer TTL
r.persist("key")
Redis Cluster : haute disponibilité et scalabilité
Architecture Cluster
Redis Cluster :
- Sharding automatique (16384 slots)
- Réplication master-replica
- Failover automatique
- Pas de SPOF (Single Point Of Failure)
Déploiement Cluster (6 nœuds : 3 masters + 3 replicas)
# Installer sur 3 serveurs
# Server 1 : 10.0.1.1
redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000 --appendonly yes
redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000 --appendonly yes
# Server 2 : 10.0.1.2
redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes-7003.conf --cluster-node-timeout 5000 --appendonly yes
redis-server --port 7004 --cluster-enabled yes --cluster-config-file nodes-7004.conf --cluster-node-timeout 5000 --appendonly yes
# Server 3 : 10.0.1.3
redis-server --port 7005 --cluster-enabled yes --cluster-config-file nodes-7005.conf --cluster-node-timeout 5000 --appendonly yes
redis-server --port 7006 --cluster-enabled yes --cluster-config-file nodes-7006.conf --cluster-node-timeout 5000 --appendonly yes
# Créer le cluster
redis-cli --cluster create \
10.0.1.1:7001 10.0.1.1:7002 \
10.0.1.2:7003 10.0.1.2:7004 \
10.0.1.3:7005 10.0.1.3:7006 \
--cluster-replicas 1
# Vérifier
redis-cli -c -p 7001 cluster info
redis-cli -c -p 7001 cluster nodes
Utilisation avec client
from redis.cluster import RedisCluster
# Connexion cluster
startup_nodes = [
{"host": "10.0.1.1", "port": "7001"},
{"host": "10.0.1.2", "port": "7003"},
{"host": "10.0.1.3", "port": "7005"}
]
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
# Utilisation normale
rc.set("key", "value")
rc.get("key")
# Hash tags pour keys sur même shard
rc.set("{user:123}:profile", "data")
rc.set("{user:123}:settings", "data")
# Les 2 clés sont sur le même nœud
Sentinel (alternative légère pour HA)
Redis Sentinel : monitoring + failover automatique sans sharding.
# sentinel.conf
sentinel monitor mymaster 10.0.1.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000
# Lancer Sentinel
redis-sentinel /etc/redis/sentinel.conf
# Client avec Sentinel
from redis.sentinel import Sentinel
sentinel = Sentinel([('10.0.1.1', 26379), ('10.0.1.2', 26379)], socket_timeout=0.1)
master = sentinel.master_for('mymaster', socket_timeout=0.1)
slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
# Écriture sur master
master.set('key', 'value')
# Lecture sur replica
value = slave.get('key')
Persistance : RDB vs AOF
RDB (Redis Database Snapshot)
Snapshot binaire à intervalles réguliers.
# redis.conf
save 900 1
save 300 10
save 60 10000
# Fichier dump
dbfilename dump.rdb
dir /var/lib/redis
Avantages :
- Compact, rapide à recharger
- Bon pour backups
Inconvénients :
- Perte possible entre 2 snapshots
- Fork() peut bloquer si dataset énorme
AOF (Append-Only File)
Log de toutes les opérations d'écriture.
# redis.conf
appendonly yes
appendfilename "appendonly.aof"
# Fsync frequency
appendfsync everysec # Recommandé
# appendfsync always # Très sûr mais lent
# appendfsync no # Rapide mais risqué
Avantages :
- Perte minimale (1s max avec everysec)
- Fichier lisible/éditable
Inconvénients :
- Fichier plus gros
- Rechargement plus lent
Stratégie hybride (recommandée)
# Activer les deux
save 900 1
appendonly yes
appendfsync everysec
# AOF rewrite auto
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
Monitoring et optimisation
Métriques essentielles
# Stats globales
redis-cli info stats
# Mémoire
redis-cli info memory
# used_memory:4GB
# used_memory_peak:5GB
# mem_fragmentation_ratio:1.05
# Clients
redis-cli info clients
# connected_clients:150
# Commandes exécutées
redis-cli info commandstats
# Clés par DB
redis-cli info keyspace
# Slow log (requêtes > 10ms)
redis-cli config set slowlog-log-slower-than 10000
redis-cli slowlog get 10
Commandes de diagnostic
# Top commandes en temps réel
redis-cli --bigkeys
# Latence
redis-cli --latency
redis-cli --latency-history
# Distribution clés par pattern
redis-cli --scan --pattern 'user:*' | wc -l
# Analyser mémoire
redis-cli --memkeys
# Export monitoring
redis-cli info all > redis_stats.txt
Prometheus exporter
# redis_exporter
docker run -d \
-p 9121:9121 \
oliver006/redis_exporter \
--redis.addr=redis://localhost:6379
# Métriques
curl localhost:9121/metrics | grep redis_
Optimisations courantes
# 1. Pipeline : batch plusieurs commandes
pipe = r.pipeline()
for i in range(1000):
pipe.set(f"key:{i}", f"value:{i}")
pipe.execute()
# 2. Éviter KEYS (bloquant) en prod
# Mauvais
keys = r.keys('user:*')
# Bon
cursor = 0
while True:
cursor, keys = r.scan(cursor, match='user:*', count=100)
# Traiter keys
if cursor == 0:
break
# 3. Utiliser HASHES pour objets liés
# Mauvais : 1 clé par champ
r.set('user:123:name', 'John')
r.set('user:123:email', 'john@example.com')
# Bon : 1 hash
r.hset('user:123', mapping={
'name': 'John',
'email': 'john@example.com'
})
# 4. Compression pour grandes valeurs
import zlib
data = zlib.compress(large_json.encode())
r.set('key', data)
# Décompression
compressed = r.get('key')
data = zlib.decompress(compressed).decode()
Sécurité Redis
Configuration sécurisée
# redis.conf
# Bind sur interface privée uniquement
bind 127.0.0.1 10.0.1.1
# Port non-standard (optionnel)
port 6380
# Password (requis en prod)
requirepass your_very_strong_password_here
# Désactiver commandes dangereuses
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG "CONFIG_SECRET_NAME"
rename-command SHUTDOWN ""
# Protected mode
protected-mode yes
# Limite clients
maxclients 10000
# TLS (Redis 6+)
tls-port 6380
tls-cert-file /path/to/redis.crt
tls-key-file /path/to/redis.key
tls-ca-cert-file /path/to/ca.crt
Client avec auth
r = redis.Redis(
host='localhost',
port=6379,
password='your_password',
ssl=True,
ssl_ca_certs='/path/to/ca.crt'
)
Firewall
# Autoriser uniquement serveurs app
ufw allow from 10.0.2.0/24 to any port 6379
ufw deny 6379
Checklist Redis Production
✅ Configuration :
- maxmemory = 75% RAM disponible
- maxmemory-policy = allkeys-lru
- Désactiver THP
- TCP backlog = 511
- lazyfree activé
✅ Sécurité :
- requirepass configuré
- bind sur IP privée
- Commandes dangereuses désactivées
- Firewall actif
- TLS si réseau non sûr
✅ Haute disponibilité :
- Redis Cluster ou Sentinel
- > = 3 masters
- 1 replica par master
- Monitoring failover
✅ Persistance :
- RDB + AOF activés
- Backups réguliers
- Test restauration
✅ Monitoring :
- Prometheus exporter
- Alertes : memory >80%, clients >80% max
- Slow log activé
- Dashboard Grafana
Conclusion
Redis est un outil puissant pour le cache haute performance quand configuré correctement. Les patterns de cache, la bonne policy d'éviction et un cluster bien dimensionné permettent de gérer des millions de requêtes par seconde.
Pour approfondir, explorez Redis Cluster pour la scalabilité horizontale, Redis Sentinel pour la haute disponibilité, et RedisStack pour des capacités avancées de recherche et stockage JSON.
Actions prioritaires :
- Configurer maxmemory et maxmemory-policy
- Implémenter cache-aside pattern
- Activer monitoring (Prometheus)
- Sécuriser avec password et bind
- Déployer Cluster pour HA si >100GB données
Gains typiques :
- Latence API : -80-95% (de 100ms à 5ms)
- Charge DB : -60-90%
- Throughput : +500-1000%
- Coûts infra DB : -50% (moins de read replicas)


