Performance
Redis
Cache

Caching Redis avancé : patterns, éviction policies, clustering

16 janvier 2026

12 min de lecture

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 :

PolicyDescriptionCas d'usage
noevictionRetourne erreur si mémoire pleineCache strict, jamais perdre données
allkeys-lruÉvict clés LRU (Least Recently Used) globalementCache 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 uniquementMix cache + données persistantes
volatile-lfuÉvict clés LFU avec TTL uniquementIdem avec fréquence
allkeys-randomÉvict clés aléatoirementCache uniforme, peu de logique
volatile-randomÉvict clés avec TTL aléatoirementRarement utile
volatile-ttlÉvict clés avec TTL le plus courtPrioriser 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 :

  1. Configurer maxmemory et maxmemory-policy
  2. Implémenter cache-aside pattern
  3. Activer monitoring (Prometheus)
  4. Sécuriser avec password et bind
  5. 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)
Besoin d'aide sur ce sujet ?

Notre équipe d'experts est là pour vous accompagner dans vos projets d'infrastructure et d'infogérance.

Contactez-nous

Articles similaires