Redis et Rails 8 : Le Guide Complet pour Développeurs Juniors

Introduction : Pourquoi Redis va changer votre façon de développer

Vous débutez avec Rails et vous entendez parler de Redis partout ? Dans les discussions sur les performances, le caching, les sessions utilisateur... Redis semble être la solution magique à tous les problèmes. Mais qu'est-ce que c'est exactement ? Et surtout, comment l'utiliser concrètement dans vos applications Rails ?


Dans cet article, nous allons explorer Redis de A à Z : ce que c'est, pourquoi c'est si rapide, comment il se compare aux bases de données classiques, et surtout comment l'intégrer facilement dans Rails 8. Préparez-vous à découvrir un outil qui va devenir indispensable dans votre boîte à outils de développeur !


Certains aspects vont peut-être vous paraître un peu complexe à comprendre. C'est normal ! Cela touche à des problématiques d'optimisation sur des applications devant gérer des milliers voir des millions d'utilisateur. Pas de panique :)

Qu'est-ce que Redis exactement ?

La définition simple

Redis (Remote Dictionary Server) est une base de données clé-valeur qui stocke toutes ses données en mémoire (RAM). Pensez-y comme à un dictionnaire géant ultra-rapide où vous pouvez stocker et récupérer des informations instantanément.


Créé en 2009 par Salvatore Sanfilippo, Redis est aujourd'hui l'une des bases de données NoSQL les plus populaires au monde. Plus de 30 000 entreprises l'utilisent, dont British Airways, HackerRank et MGM Resorts. En 2025, Redis 8.2 est la dernière version majeure disponible, apportant des améliorations de performance impressionnantes : jusqu'à 91% plus rapide que Redis 7.2 !

Le concept clé : la mémoire vive

La différence fondamentale entre Redis et une base de données traditionnelle comme PostgreSQL ou MySQL ? L'emplacement du stockage.

  1. Base de données classique : stocke les données sur le disque dur (HDD ou SSD)
  2. Redis : stocke les données dans la RAM


Cette différence peut sembler mineure, mais elle est énorme en termes de performance :

  1. Accès à la RAM : 100 nanosecondes
  2. Accès à un SSD : 100 microsecondes (1 000 fois plus lent !)


100 microsecondes ça parait déjà pas grand chose vous allez me dire ? Prenez l'analogie suivante : Imaginez que vous cherchiez un livre dans votre bibliothèque. Avec une base de données classique, vous devez aller dans une autre pièce pour le trouver. Avec Redis, le livre est déjà dans votre main.

Les structures de données de Redis

Redis n'est pas qu'un simple stockage clé-valeur. Il propose des structures de données avancées qui le rendent extrêmement versatile. C'est un peu comme avoir plusieurs types de conteneurs pour ranger différents types d'objets.


Ci-dessous, je vous présente quelques exemples de comment on structure les données dans Redis.

1. Les Strings (Chaînes)

La structure la plus basique. Une clé pointe vers une valeur texte (ou binaire).


Exemple d'utilisation :

# Stocker un compteur de vues
REDIS.set("article:123:views", 42)
REDIS.get("article:123:views")
# => "42"
# Incrémenter directement dans Redis
REDIS.incr("article:123:views")
# => 43


Cas d'usage parfaits :

  1. Compteurs (vues d'articles, likes)
  2. Cache de fragments HTML
  3. Tokens de session temporaires
  4. Valeurs de configuration


Capacité : jusqu'à 512 MB par valeur !

2. Les Hashes (Tables de hachage)

Imaginez un hash Ruby stocké dans Redis. C'est parfait pour représenter des objets avec plusieurs attributs.


Exemple d'utilisation :

# Stocker les informations d'un utilisateur
REDIS.hset("user:1", "name", "Alice")
REDIS.hset("user:1", "email", "alice@example.com")
REDIS.hset("user:1", "age", "28")
# Récupérer toutes les données
REDIS.hgetall("user:1")
# => {"name"=>"Alice", "email"=>"alice@example.com", "age"=>"28"}
# Récupérer un champ spécifique
REDIS.hget("user:1", "email")
# => "alice@example.com"


Cas d'usage parfaits :

  1. Profils utilisateurs
  2. Configurations d'objets
  3. Métriques multi-tenant
  4. Données structurées légères


Avantage : très efficace en mémoire pour stocker de nombreux petits objets

3. Les Lists (Listes)

Des listes ordonnées d'éléments. Pensez à un array Ruby, mais dans Redis. Parfait pour les files d'attente !


Exemple d'utilisation :

# Ajouter des tâches à une file d'attente
REDIS.lpush("jobs:queue", "send_email")
REDIS.lpush("jobs:queue", "process_payment")
REDIS.lpush("jobs:queue", "generate_report")
# Traiter les tâches (FIFO)
REDIS.rpop("jobs:queue")
# => "send_email"
# Voir les 5 premières tâches sans les retirer
REDIS.lrange("jobs:queue", 0, 4)


Cas d'usage parfaits :

  1. Files d'attente de messages
  2. Timeline d'activités (fil d'actualité)
  3. Historique des dernières actions
  4. Système de notifications


Note importante : les opérations en début et fin de liste sont en O(1) - ultra-rapides !

4. Les Sets (Ensembles)

Collections d'éléments uniques non ordonnés. Exactement comme un Set en Ruby.


Exemple d'utilisation :

# Tags d'un article
REDIS.sadd("article:123:tags", "ruby", "rails", "redis")
REDIS.sadd("article:123:tags", "ruby") # Ne sera pas ajouté deux fois
# Vérifier l'appartenance
REDIS.sismember("article:123:tags", "ruby")
# => true
# Opérations ensemblistes
REDIS.sadd("article:456:tags", "ruby", "javascript")
REDIS.sinter("article:123:tags", "article:456:tags")
# => ["ruby"] # Intersection


Cas d'usage parfaits :

  1. Systèmes de tags
  2. Listes d'amis/followers uniques
  3. Tracking d'adresses IP uniques
  4. Relations many-to-many simples


Bonus : opérations ensemblistes (union, intersection, différence) en O(N)

5. Les Sorted Sets (Ensembles triés)

La structure la plus puissante ! Des éléments uniques avec un score qui détermine l'ordre.


Exemple d'utilisation :

# Classement de joueurs avec leur score
REDIS.zadd("leaderboard", 1500, "player:alice")
REDIS.zadd("leaderboard", 2200, "player:bob")
REDIS.zadd("leaderboard", 1800, "player:charlie")
# Top 3 des meilleurs joueurs
REDIS.zrevrange("leaderboard", 0, 2, with_scores: true)
# => [["player:bob", 2200.0], ["player:charlie", 1800.0], ["player:alice", 1500.0]]
# Classement d'un joueur spécifique
REDIS.zrevrank("leaderboard", "player:alice")
# => 2 # Troisième position


Cas d'usage parfaits :

  1. Leaderboards (classements)
  2. Priority queues (files de priorité)
  3. Rate limiting (limitation de débit)
  4. Données temporelles ordonnées

Performance : toutes les opérations de base en O(log N)

Redis vs Bases de données traditionnelles : Le match


Maintenant que vous connaissez Redis, comparons-le avec PostgreSQL ou MySQL pour comprendre quand utiliser l'un ou l'autre.

Tableau comparatif

Critère
Redis
PostgreSQL/MySQL
Stockage
En mémoire (RAM)
Sur disque (HDD/SSD)
Vitesse de lecture
Sub-milliseconde
Millisecondes à secondes
Modèle de données
Clé-valeur + structures
Relationnel (tables)
Requêtes complexes
Limitées
SQL complet, JOINs
Transactions ACID (*)
Partielles
Complètes
Persistance
Optionnelle
Native
Capacité
Limitée par RAM
Limitée par disque
Coût
RAM => plus chère
Disque Dur => moins cher

(*) Petit Zoom sur les transactions "ACID" : qu'est-ce que c'est ?

Une transaction ACID est une opération sur une base de données qui respecte quatre propriétés essentielles : Atomicité, Cohérence, Isolation et Durabilité. Ces propriétés garantissent la fiabilité et l'intégrité des données, même en cas d'erreur ou de panne.


🔹 Atomicité

  1. Tout ou rien : une transaction est indivisible. Soit toutes les opérations qu’elle contient sont exécutées, soit aucune ne l’est.
  2. Exemple : lors d’un virement bancaire, si le débit du compte A échoue, le crédit du compte B ne doit pas avoir lieu non plus.

🔹 Cohérence

  1. La base de données passe d’un état valide à un autre état valide après la transaction.
  2. Les règles d’intégrité (contraintes, relations, etc.) doivent toujours être respectées.
  3. Exemple : un inventaire ne doit jamais contenir un nombre négatif d’articles après une mise à jour.

🔹 Isolation

  1. Les transactions concurrentes ne doivent pas interférer entre elles.
  2. Chaque transaction doit s’exécuter comme si elle était seule sur le système.
  3. Cela évite les effets de bord comme les lectures de données non validées par une autre transaction.

🔹 Durabilité

  1. Une fois qu’une transaction est validée (commit), ses effets sont permanents, même en cas de panne système.
  2. Les données modifiées sont enregistrée


REDIS n'offre qu'un support partiel puisque il ne garanti que l'atomicité et une certaine forme d'isolation. Il n'assure pas la cohérence ni la durabilité.


Atomicité

  1. Redis garantit que les commandes d’une transaction sont exécutées ensemble ou pas du tout.
  2. Si une erreur survient avant l’exécution rien n’est appliqué.
  3. Mais attention : Redis n’annule pas les commandes individuellement si l’une échoue après exécution.

⚠️ Cohérence

  1. Redis ne vérifie pas automatiquement les contraintes d’intégrité comme les bases relationnelles.
  2. La cohérence dépend donc de la logique de l’application, pas du moteur Redis lui-même.
  3. Dans une base de donnée relationnelle comme Postgre c'est notamment assuré par le schéma que l'on doit suivre à la lettre pour insérer des informations.

Isolation

  1. Redis exécute les commandes d’une transaction séquentiellement et sans interruption par d’autres clients.
  2. Cela garantit une isolation simple, mais pas aussi fine que les niveaux d’isolation des SGBD relationnels.

⚠️ Durabilité

  1. Redis est une base in-memory, donc les données sont en RAM. En gros, s'il y a un crash votre mémoire vive se vide et vous avez tout perdu.
  2. Il y a quand même des modes de fonctionnement qui permettent d'améliorer la durabilité mais cela vient avec un coût bien évidemment :
  3. RDB (snapshot) : sauvegarde périodique, risque de perte entre snapshots.
  4. AOF (append-only file) : meilleure durabilité, mais peut être désactivée ou configurée avec des délais.
  5. En cas de crash, certaines données peuvent être perdues si elles n’ont pas été écrites sur disque.

Quand utiliser une base de données classique ?

✅ PostgreSQL/MySQL est parfait pour :

  1. Données persistantes : Tout ce qui doit survivre à un redémarrage
  2. Relations complexes : JOINs, foreign keys, contraintes
  3. Requêtes analytiques : Agrégations, GROUP BY, calculs complexes
  4. Données volumineuses : Plusieurs téraoctets de données
  5. Intégrité des données : Transactions ACID complètes
  6. Historique complet : Vous ne voulez jamais perdre ces données

L'approche hybride (la meilleure !)

Dans la réalité, vous utiliserez les deux :


# PostgreSQL pour les données de base
user = User.find(params[:id])
# Redis pour le cache
@posts = Rails.cache.fetch("user:#{user.id}:posts", expires_in: 5.minutes) do
user.posts.published.includes(:comments).to_a
end
# Redis pour les compteurs temps réel
@views_count = REDIS.get("user:#{user.id}:profile_views").to_i
REDIS.incr("user:#{user.id}:profile_views")


Principe général :

  1. PostgreSQL = Source de vérité, données permanentes
  2. Redis = Couche de vitesse, données temporaires ou dérivées

Exemple d'utilisation de Redis

Voici plusieurs exemples concrets d'utilisation de Redis.

  1. Le caching : Stocker temporairement des données coûteuses à calculer
# Cache d'une requête complexe qui évite de surcharger PostgreSQL
Rails.cache.fetch("user:#{user.id}:dashboard", expires_in: 15.minutes) do
# Cette requête coûteuse ne s'exécute que si le cache est vide
{
posts: user.posts.includes(:comments).recent.to_a,
stats: user.calculate_stats,
notifications: user.notifications.unread.to_a
}
end

Pourquoi ? Évite de marteler PostgreSQL avec les mêmes requêtes. Le résultat est en RAM, instantané à récupérer.


  1. Les sessions utilisateur : Données éphémères qui doivent être ultra-rapides
# Session stockée dans Redis au lieu de cookies (ou de PostgreSQL)
config.session_store :redis_store,
servers: ["redis://localhost:6379/0/session"],
expire_after: 90.minutes

Pourquoi ? Redis peut gérer des milliers d'accès session/seconde sans broncher. PostgreSQL ralentirait avec ce pattern d'accès ultra-fréquent. Bonus : vous pouvez invalider les sessions instantanément (logout partout).


  1. Les compteurs temps réel avec synchronisation différée : Likes, vues, statistiques live
# Pattern hybride : Redis pour la vitesse, PostgreSQL pour la persistance
# 1. Incrémenter dans Redis (instantané, pas de lock sur PostgreSQL)
REDIS.incr("article:#{@article.id}:views")
# 2. Job en arrière-plan pour synchroniser vers PostgreSQL toutes les 5 minutes
class SyncViewCountsJob < ApplicationJob
def perform
Article.find_each do |article|
redis_count = REDIS.get("article:#{article.id}:views").to_i
if redis_count > 0
article.increment!(:view_count, redis_count)
REDIS.del("article:#{article.id}:views")
end
end
end
end

Pourquoi ? Les opérations d'incrémentation sont atomiques dans Redis (pas de race conditions) et ne créent aucun lock sur PostgreSQL. Vous pouvez absorber 10 000 incrémentations/seconde sans ralentir votre DB principale. PostgreSQL lui va garder uniquement la valeur définitive.


  1. Les files d'attente de jobs : Avec Sidekiq par exemple
# Sidekiq utilise les Lists Redis comme file d'attente
SendEmailJob.perform_later(user_id: @user.id)
# Dans Redis, cela crée :
# LPUSH "queue:default" "{class: 'SendEmailJob', args: [123]}"
# RPOP "queue:default" # Le worker récupère le job

Pourquoi ? Les Lists Redis offrent des opérations LPUSH/RPOP atomiques ultra-rapides. PostgreSQL ne peut pas battre cette performance pour des files d'attente à haute fréquence. Redis peut gérer des millions de jobs par heure.


  1. Les leaderboards et classements : Parfait avec les Sorted Sets
# Redis pour le TOP N seulement (par exemple top 1000)
# On ne garde PAS tous les joueurs en mémoire !
# Mise à jour du score
def update_leaderboard(player_id, score)
REDIS.zadd("game:leaderboard", score, player_id)
# IMPORTANT : Garder seulement le top 1000 pour ne pas exploser la RAM
current_size = REDIS.zcard("game:leaderboard")
if current_size > 1000
REDIS.zremrangebyrank("game:leaderboard", 0, current_size - 1001)
end
end
# Afficher le top 10 instantanément
top_10_ids = REDIS.zrevrange("game:leaderboard", 0, 9, with_scores: true)
# Pour un joueur pas dans le top 1000 :
def player_rank(player_id)
redis_rank = REDIS.zrevrank("game:leaderboard", player_id)
if redis_rank.nil?
# Pas dans le top 1000 Redis, aller chercher dans PostgreSQL
Player.where("score > ?", Player.find(player_id).score).count + 1
else
redis_rank + 1 # Redis indexe à partir de 0
end
end
# PostgreSQL garde TOUS les scores pour l'historique et les classements généraux
class Player < ApplicationRecord
# Index sur score pour les requêtes de classement
# add_index :players, :score
end

Pourquoi ? Les Sorted Sets de Redis maintiennent l'ordre automatiquement en O(log N) pour le top N (1000 joueurs). C'est parfait pour afficher les leaders. Pour les joueurs en dehors du top, PostgreSQL prend le relais.


Trade-off important :

  1. ✅ Top 1000 en Redis : accès instantané, mises à jour ultra-rapides
  2. ✅ Tous les joueurs en PostgreSQL : classement complet disponible
  3. ⚠️ Si vous avez 10 millions de joueurs et que vous mettez tout dans Redis, vous consommez énormément de RAM (évitez !)
  4. 💡 Règle générale : Redis pour le "hot data" (top/actifs), PostgreSQL pour le "cold data" (historique/complet)


  1. Le pub/sub temps réel : Pour ActionCable et WebSockets
# Redis comme backend pour ActionCable
# Permet la communication entre serveurs Rails dans un déploiement multi-instances
config.action_cable.cable = {
adapter: 'redis',
url: ENV['REDIS_URL']
}
# Quand un user poste un message :
ActionCable.server.broadcast(
"chat:#{room.id}",
message: message.as_json
)
# Redis distribue instantanément à tous les serveurs connectés

Pourquoi ? Redis Pub/Sub est non-bloquant et peut gérer des milliers de messages par seconde. PostgreSQL LISTEN/NOTIFY existe mais n'est pas conçu pour ce débit. Redis permet aussi la scalabilité horizontale de votre app (plusieurs serveurs Rails partageant le même Redis).


  1. Le rate limiting distribué : Limiter les requêtes API
# Limiter à 100 requêtes par heure par utilisateur
key = "rate_limit:user:#{user.id}:#{Time.current.hour}"
count = REDIS.incr(key)
REDIS.expire(key, 1.hour) if count == 1
if count > 100
render json: { error: "Rate limit exceeded" }, status: 429
else
# Traiter la requête
end

Pourquoi ? Les opérations INCR + EXPIRE sont atomiques dans Redis. Impossible d'avoir une race condition même avec 10 serveurs Rails en parallèle. PostgreSQL aurait des problèmes de locks et de performance avec ce pattern d'accès.



Besoin
Redis
PostgreSQL
Vitesse pure
⚡⚡⚡⚡⚡
⚡⚡⚡
Opérations atomiques
⚡⚡⚡⚡⚡ (natif)
⚡⚡⚡ (locks requis)
Haute fréquence écriture
⚡⚡⚡⚡⚡
⚡⚡ (contention)
Durabilité garantie
⚡⚡ (optionnelle)
⚡⚡⚡⚡⚡
Requêtes complexes
⚡ (limitées)
⚡⚡⚡⚡⚡
Relations
⚡⚡⚡⚡⚡
Capacité
⚡⚡ (RAM limitée)
⚡⚡⚡⚡⚡ (disque)
Coût
⚡⚡ (RAM chère)
⚡⚡⚡⚡ (disque pas cher)


Rails 8 et Redis : Une nouvelle ère

Rails 8, sorti en 2024, a introduit un changement majeur dans l'écosystème : Solid Cache.

Solid Cache : Le nouveau paradigme

Solid Cache est une alternative à Redis pour le caching, utilisant votre base de données plutôt que la mémoire. Surprenant ? Oui. Contre-intuitif ? Pas tant que ça !


Le concept : avec les SSD modernes et les caches en mémoire intégrés aux bases de données, stocker le cache sur disque devient viable. L'avantage ? Vous pouvez avoir un cache énorme pour une fraction du coût de la RAM.


Les chiffres de Basecamp :

  1. Lectures 40% plus lentes qu'avec Redis
  2. Mais cache 6x plus grand !
  3. Résultat : temps de réponse du 95e percentile passé de 375ms à 225ms


Configuration dans Rails 8 :

# config/environments/production.rb
config.cache_store = :solid_cache_store


Par défaut, Rails 8 utilise Solid Cache au lieu de Redis. C'est un choix pragmatique pour simplifier le déploiement.

Alors, Redis est mort dans Rails 8 ?

Absolument pas ! Redis reste essentiel pour :

  1. Les cas où la vitesse prime : Rate limiting, sessions à haute fréquence
  2. Les structures de données spéciales : Sorted Sets pour leaderboards
  3. Sidekiq : Toujours basé sur Redis pour les jobs
  4. ActionCable en production : Redis recommandé pour le pub/sub
  5. Les compteurs atomiques : Incrémentations ultra-rapides


La bonne approche :

# Solid Cache pour le cache général (fragment cache, requêtes)
config.cache_store = :solid_cache_store
# Redis pour le rate limiting spécifique
rate_limit to: 10, within: 1.minute,
store: ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])

Le dev container Rails 8

Point important : Rails 8 ne génère plus Redis par défaut dans les dev containers car il utilise Solid Queue et Solid Cache. Si vous avez besoin de Redis (pour Sidekiq ou autre), ajoutez --skip-solid lors de la création de l'app :


rails new mon_app --skip-solid

Installation et configuration de Redis avec Rails

Passons à la pratique ! Voici comment installer et configurer Redis dans votre projet Rails.

Étape 1 : Installer Redis sur votre système

macOS (avec Homebrew) :

# Installer Redis
brew install redis
# Lancer Redis automatiquement au démarrage
brew tap homebrew/services
brew services start redis
# Vérifier que Redis fonctionne
redis-cli ping
# PONG

Ubuntu/Debian :

# Installer Redis
sudo apt update
sudo apt install redis-server
# Démarrer Redis
sudo systemctl start redis-server
# Activer Redis au démarrage
sudo systemctl enable redis-server
# Vérifier
redis-cli ping
# PONG

Avec Docker :

# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7.2-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
redis-data:

Étape 2 : Ajouter les gems nécessaires

# Gemfile
gem 'redis', '~> 5.0' # Client Redis officiel
gem 'redis-namespace' # Namespace pour isolation (optionnel mais recommandé)
bundle install

Étape 3 : Configurer la connexion Redis

Créez un initializer pour la connexion globale :

# config/initializers/redis.rb
require 'redis'
require 'redis-namespace'
redis_host = ENV.fetch('REDIS_HOST', 'localhost')
redis_port = ENV.fetch('REDIS_PORT', 6379)
redis_db = ENV.fetch('REDIS_DB', 0)
# Connexion globale
redis_connection = Redis.new(
host: redis_host,
port: redis_port,
db: redis_db,
timeout: 5,
reconnect_attempts: 3
)
# Avec namespace pour isolation
REDIS = Redis::Namespace.new(
"#{Rails.application.class.module_parent_name.underscore}:#{Rails.env}",
redis: redis_connection
)

Pourquoi le namespace ? Si vous avez plusieurs apps Rails utilisant le même Redis, le namespace évite les collisions de clés. Par exemple :

  1. Sans namespace : user:1
  2. Avec namespace : mon_app:production:user:1

Étape 4 : Configurer Redis comme cache store

Pour utiliser Redis comme cache :

# config/environments/production.rb
config.cache_store = :redis_cache_store, {
url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
namespace: 'cache',
expires_in: 90.minutes,
# Options de reconnexion
reconnect_attempts: 3,
reconnect_delay: 0.5,
reconnect_delay_max: 2.0,
# Gestion des erreurs
error_handler: -> (method:, returning:, exception:) {
Rails.logger.error("Redis error: #{exception.message}")
}
}

En développement :

# config/environments/development.rb
config.cache_store = :redis_cache_store, {
url: 'redis://localhost:6379/0',
namespace: 'cache',
expires_in: 30.minutes
}
config.action_controller.perform_caching = true

Étape 5 : Utiliser Redis dans votre code

Cache basique :

# Dans un contrôleur ou modèle
Rails.cache.fetch("user:#{user.id}:profile", expires_in: 1.hour) do
# Cette requête ne sera exécutée que si le cache est vide
user.profile.to_json
end

Compteur simple :

# Incrémenter un compteur
REDIS.incr("article:#{@article.id}:views")
# Lire le compteur
views = REDIS.get("article:#{@article.id}:views").to_i

Set pour tracking :

# Ajouter un visiteur unique
REDIS.sadd("article:#{@article.id}:unique_visitors", current_user.id)
# Compter les visiteurs uniques
unique_count = REDIS.scard("article:#{@article.id}:unique_visitors")

Leaderboard avec Sorted Set :

# Ajouter/mettre à jour un score
REDIS.zadd("game:leaderboard", @player.score, @player.id)
# Top 10
top_players = REDIS.zrevrange("game:leaderboard", 0, 9, with_scores: true)
top_players.each_with_index do |(player_id, score), index|
puts "#{index + 1}. Player #{player_id}: #{score.to_i} points"
end

Étape 6 : Configuration pour Sidekiq (jobs en arrière-plan)

Si vous utilisez Sidekiq pour les jobs asynchrones :

# Gemfile
gem 'sidekiq', '~> 7.0'
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') }
end
# config/application.rb
config.active_job.queue_adapter = :sidekiq

Patterns et bonnes pratiques

Maintenant que Redis est installé, voici les patterns à connaître pour l'utiliser efficacement. Certains exemples sont assez avancés. Pas de panique vous n'aurez pas à les implémenter tout de suite mais c'est quand même bien de les garder dans un coin de votre tête :)

1. Nommage des clés : soyez structuré !

Principe : utilisez des clés descriptives et hiérarchiques avec : comme séparateur.

# ❌ Mauvais
REDIS.set("u1", "data")
REDIS.set("userdata", "other")
# ✅ Bon
REDIS.set("user:#{user_id}:profile", data)
REDIS.set("user:#{user_id}:stats:views", count)
REDIS.set("article:#{article_id}:comments:count", count)

Convention recommandée :

resource:id:attribute
# ou
resource:id:sub_resource:attribute

2. Expiration automatique : nettoyez après vous

Toujours définir un TTL (Time To Live) pour éviter que Redis ne se remplisse :

# Expiration lors du set
REDIS.setex("session:#{session_id}", 1.hour.to_i, session_data)
# Ou après
REDIS.set("cache:key", data)
REDIS.expire("cache:key", 30.minutes.to_i)
# Avec Rails.cache (automatique)
Rails.cache.fetch("key", expires_in: 1.hour) { expensive_operation }


Pourquoi c'est crucial ? Redis stocke tout en RAM. Sans expiration, vous risquez de manquer de mémoire. En expirant cela vide la mémoire.

3. Atomic operations : soyez thread-safe

Redis garantit l'atomicité des opérations. Utilisez-le pour les compteurs !

# ❌ Pas thread-safe
current = REDIS.get("counter").to_i
REDIS.set("counter", current + 1)
# Entre ces deux lignes, un autre process peut modifier la valeur !
# ✅ Thread-safe et atomic
REDIS.incr("counter")
REDIS.decr("counter")
REDIS.incrby("counter", 10)

4. Pipelining : groupez les opérations

Quand vous avez plusieurs commandes Redis à exécuter, utilisez le pipelining :

# ❌ Lent (3 allers-retours réseau)
REDIS.set("key1", "value1")
REDIS.set("key2", "value2")
REDIS.set("key3", "value3")
# ✅ Rapide (1 seul aller-retour)
REDIS.pipelined do |pipeline|
pipeline.set("key1", "value1")
pipeline.set("key2", "value2")
pipeline.set("key3", "value3")
end

Gain de performance : jusqu'à 10x plus rapide pour de nombreuses opérations !

5. Cache warming : préchauffez le cache

Le problème du "cold start" :

Imaginez votre application Redis vient de redémarrer (maintenance, crash, ou simplement première mise en production). Votre cache est vide.


Voici ce qui se passe :

# Premier utilisateur arrive sur la page d'accueil
@popular_articles = Rails.cache.fetch("homepage:popular", expires_in: 1.hour) do
# ❌ CACHE MISS ! La requête doit s'exécuter
Article.published
.includes(:author, :comments)
.order(views_count: :desc)
.limit(10)
.to_a
end
# Temps : 500ms (requête lourde + N+1 queries potentielles)

Le premier utilisateur subit toute la lenteur pendant que le cache se remplit. Puis tous les autres utilisateurs bénéficient du cache pendant 1 heure. Mais quand le cache expire ou que Redis redémarre, c'est reparti : un utilisateur "chanceux" prend la pénalité.


La solution : Cache Warming

Au lieu d'attendre qu'un utilisateur déclenche le calcul, vous préchauffez le cache de manière proactive :


Exemple 1 : Warming au démarrage de l'application


# config/initializers/cache_warming.rb
Rails.application.config.after_initialize do
# Seulement en production et pas pendant les migrations
if Rails.env.production? && !defined?(Rails::Console)
CacheWarmingJob.perform_later
end
end
# app/jobs/cache_warming_job.rb
class CacheWarmingJob < ApplicationJob
queue_as :low_priority # Ne pas bloquer les jobs critiques
def perform
Rails.logger.info "🔥 Starting cache warming..."
# 1. Articles populaires de la homepage
Rails.cache.fetch("homepage:popular", expires_in: 1.hour) do
Article.published
.includes(:author, :comments)
.order(views_count: :desc)
.limit(10)
.to_a
end
# 2. Statistiques du site
Rails.cache.fetch("site:stats", expires_in: 30.minutes) do
{
total_users: User.count,
total_articles: Article.count,
total_comments: Comment.count,
active_today: User.where("last_seen_at > ?", 24.hours.ago).count
}
end
# 3. Top 10 des auteurs
Rails.cache.fetch("authors:top", expires_in: 2.hours) do
User.joins(:articles)
.select("users.*, COUNT(articles.id) as articles_count")
.group("users.id")
.order("articles_count DESC")
.limit(10)
.to_a
end
# 4. Catégories avec compteurs
Category.find_each do |category|
Rails.cache.fetch("category:#{category.id}:articles_count", expires_in: 1.hour) do
category.articles.published.count
end
end
Rails.logger.info "✅ Cache warming completed!"
end
end

Résultat : Quand le premier utilisateur arrive, le cache est déjà chaud. Temps de réponse : 5ms au lieu de 500ms !


Exemple 2 : Warming périodique avec Sidekiq-Cron

Si votre cache a une durée de vie courte ou si certaines pages sont très importantes, vous pouvez préchauffer régulièrement avant l'expiration :

# Gemfile
gem 'sidekiq-cron'
# config/schedule.yml (pour Sidekiq-Cron)
cache_warming:
cron: "*/15 * * * *" # Toutes les 15 minutes
class: "CacheWarmingJob"
queue: low_priority
homepage_critical:
cron: "*/5 * * * *" # Toutes les 5 minutes
class: "HomepageCacheWarmingJob"
queue: critical


# app/jobs/homepage_cache_warming_job.rb
class HomepageCacheWarmingJob < ApplicationJob
queue_as :critical
def perform
# Préchauffer les caches critiques AVANT qu'ils n'expirent
# Ainsi, les utilisateurs ont TOUJOURS un cache chaud
warm_cache("homepage:hero", 10.minutes) do
Article.featured.includes(:author, :tags).first
end
warm_cache("homepage:trending", 10.minutes) do
Article.trending_last_24h.limit(5).to_a
end
warm_cache("homepage:categories", 30.minutes) do
Category.with_article_counts.to_a
end
end
private
def warm_cache(key, expires_in)
# Forcer le refresh du cache même s'il existe
Rails.cache.delete(key)
Rails.cache.fetch(key, expires_in: expires_in) do
yield
end
end
end


Quand utiliser le cache warming ?


✅ Utilisez le cache warming si :

  1. Vous avez des pages à fort trafic avec des requêtes lourdes
  2. Votre cache Redis redémarre régulièrement
  3. Vous voulez des temps de réponse prévisibles
  4. Vos requêtes prennent > 100ms à calculer
  5. Vous avez des "moments de pointe" prévisibles (ex: lancement produit)

❌ N'utilisez PAS le cache warming si :

  1. Vos données changent en permanence (temps réel pur)
  2. Vous avez peu de trafic (pas de bénéfice)
  3. Vos requêtes sont déjà rapides (< 50ms)
  4. Vous avez trop de variations de données à préchauffer (millions de combinaisons)


Le cache warming comme "assurance performance"


Pensez au cache warming comme à une assurance :

  1. Coût : Quelques jobs en arrière-plan (ressources serveur)
  2. Bénéfice : Expérience utilisateur constamment rapide
  3. Moment optimal : Juste avant l'expiration du cache ou après un redémarrage

Analogie : C'est comme préchauffer votre four avant d'y mettre le plat. Vous pourriez attendre qu'il chauffe quand vous mettez le plat (utilisateur attend), ou le préchauffer avant (utilisateur heureux immédiatement).

6. Gestion des erreurs : prévoyez les pannes

Redis peut tomber. Votre app ne doit pas crasher pour autant :

# Wrapper safe pour les opérations Redis
module SafeRedis
def self.with_rescue(default: nil)
yield
rescue Redis::BaseError => e
Rails.logger.error("Redis error: #{e.message}")
default
end
end
# Utilisation
views_count = SafeRedis.with_rescue(default: 0) do
REDIS.get("article:#{id}:views").to_i
end

Pour le cache Rails, la gestion d'erreur est intégrée :

config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
error_handler: -> (method:, returning:, exception:) {
# Logger l'erreur
Rails.logger.error("Redis cache error: #{exception.message}")
# Alerter l'équipe si nécessaire
ErrorNotifier.notify(exception) if Rails.env.production?
}
}

7. Monitoring : surveillez les métriques

Commandes utiles pour surveiller Redis :

# Informations générales
redis-cli INFO
# Mémoire utilisée
redis-cli INFO memory
# Statistiques des commandes
redis-cli INFO stats
# Clés par pattern
redis-cli KEYS "user:*" | wc -l
# Surveiller les commandes en temps réel
redis-cli MONITOR

Dans votre code Rails :

# Obtenir des stats
info = REDIS.info
memory_used = info["used_memory_human"]
connected_clients = info["connected_clients"]
total_commands = info["total_commands_processed"]


8. Gestion de la mémoire : n'oubliez jamais la limite !

Le problème : Redis stocke TOUT en RAM. Si vous remplissez la mémoire, Redis crashe ou commence à rejeter les commandes.


Il faut commencer par définir une limite mémoire dans la configuration de redis


# Dans redis.conf
maxmemory 2gb
# Politique d'éviction quand la limite est atteinte
maxmemory-policy allkeys-lru


Politiques d'éviction disponibles :

  1. allkeys-lru : Supprime les clés les moins récemment utilisées (recommandé pour cache)
  2. volatile-lru : Supprime seulement les clés avec TTL
  3. allkeys-lfu : Supprime les clés les moins fréquemment utilisées (Redis 4.0+)
  4. volatile-ttl : Supprime les clés avec le TTL le plus court
  5. noeviction : Rejette les écritures quand plein (dangereux !)


Vous pouvez également limite la taille des collections


# ❌ Dangereux : Collection sans limite
def add_to_recent_activity(user_id, activity)
REDIS.lpush("user:#{user_id}:activity", activity.to_json)
# Cette liste peut croître infiniment !
end
# ✅ Sûr : Limiter à N éléments
def add_to_recent_activity(user_id, activity)
REDIS.lpush("user:#{user_id}:activity", activity.to_json)
REDIS.ltrim("user:#{user_id}:activity", 0, 99) # Garder seulement 100 items
end
# ✅ Alternative : Sorted Set avec limite
def add_to_leaderboard(player_id, score)
REDIS.zadd("leaderboard", score, player_id)
# Garder seulement le top 1000
total = REDIS.zcard("leaderboard")
REDIS.zremrangebyrank("leaderboard", 0, total - 1001) if total > 1000
end


Faire du monitoring de l'utilisation de la mémoire


# Estimer la mémoire d'une clé
REDIS.memory("usage", "user:123:profile")
# => 1024 (bytes)
# Scanner et mesurer toutes les clés d'un pattern
total_memory = 0
REDIS.scan_each(match: "user:*", count: 100) do |key|
total_memory += REDIS.memory("usage", key)
end
puts "Total memory used by user:* keys: #{total_memory / 1024 / 1024}MB"

Bien gérer le passage donnée chaude / donnée froide


# Principe : Redis pour les données "chaudes" (accès fréquent)
# PostgreSQL pour les données "froides" (historique)
class ActivityLog
# Garder les 30 derniers jours dans Redis
def self.recent(user_id)
cached = REDIS.lrange("user:#{user_id}:activities", 0, 99)
cached.map { |json| JSON.parse(json) }
end
# PostgreSQL pour l'historique complet
def self.all_time(user_id)
Activity.where(user_id: user_id).order(created_at: :desc)
end
def self.add(user_id, activity)
# Redis pour accès rapide
REDIS.lpush("user:#{user_id}:activities", activity.to_json)
REDIS.ltrim("user:#{user_id}:activities", 0, 99)
REDIS.expire("user:#{user_id}:activities", 30.days)
# PostgreSQL pour persistance
Activity.create!(user_id: user_id, data: activity)
end
end


Règles d'or pour la mémoire :

  1. 📏 Toujours définir maxmemory en production
  2. ⏱️ Toujours définir des TTL sur vos clés
  3. 📊 Limiter la taille des collections (Lists, Sets, Sorted Sets)
  4. 🔍 Monitorer l'utilisation mémoire régulièrement
  5. 🔥 Garder seulement le "hot data" dans Redis
  6. 💾 Utiliser PostgreSQL comme source de vérité et archive

Cas pratiques : exemples concrets

Voyons quelques implémentations réelles que vous rencontrerez souvent.

Exemple 1 : Cache de requêtes coûteuses

# app/models/user.rb
class User < ApplicationRecord
def dashboard_data
Rails.cache.fetch("user:#{id}:dashboard", expires_in: 15.minutes) do
{
recent_posts: posts.recent.limit(5).to_a,
stats: calculate_stats,
notifications: notifications.unread.to_a,
activity_feed: generate_activity_feed
}
end
end
private
def calculate_stats
{
total_posts: posts.count,
total_comments: comments.count,
followers: followers.count,
following: following.count
}
end
end

Exemple 2 : Rate limiting

# app/controllers/concerns/rate_limitable.rb
module RateLimitable
extend ActiveSupport::Concern
def rate_limit!(key, max_requests: 100, period: 1.hour)
redis_key = "rate_limit:#{key}"
current_count = REDIS.get(redis_key).to_i
if current_count >= max_requests
render json: { error: "Rate limit exceeded" }, status: :too_many_requests
return false
end
REDIS.multi do |multi|
multi.incr(redis_key)
multi.expire(redis_key, period.to_i) if current_count == 0
end
true
end
end
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
include RateLimitable
before_action -> { rate_limit!("api:#{current_user.id}", max_requests: 1000) }
def index
@posts = Post.all
render json: @posts
end
end

Exemple 3 : Leaderboard de jeu

# app/models/game_leaderboard.rb
class GameLeaderboard
REDIS_KEY = "game:leaderboard"
def self.update_score(player_id, score)
REDIS.zadd(REDIS_KEY, score, player_id)
end
def self.top(limit = 10)
player_ids_with_scores = REDIS.zrevrange(REDIS_KEY, 0, limit - 1, with_scores: true)
player_ids_with_scores.map do |player_id, score|
{
player: Player.find(player_id),
score: score.to_i,
rank: rank(player_id)
}
end
end
def self.rank(player_id)
REDIS.zrevrank(REDIS_KEY, player_id)&.+ 1
end
def self.score(player_id)
REDIS.zscore(REDIS_KEY, player_id)&.to_i
end
def self.player_stats(player_id)
{
score: score(player_id),
rank: rank(player_id),
total_players: REDIS.zcard(REDIS_KEY)
}
end
end
# Utilisation
GameLeaderboard.update_score(player.id, 2500)
top_players = GameLeaderboard.top(10)
my_stats = GameLeaderboard.player_stats(current_player.id)

Exemple 4 : Session store pour authentification

# config/initializers/session_store.rb
Rails.application.config.session_store :redis_store,
servers: [
{
host: ENV.fetch('REDIS_HOST', 'localhost'),
port: ENV.fetch('REDIS_PORT', 6379),
db: 1,
namespace: 'session'
}
],
expire_after: 90.minutes,
key: "_#{Rails.application.class.module_parent_name.underscore}_session",
secure: Rails.env.production?,
httponly: true,
same_site: :lax

Exemple 5 : Pub/Sub pour notifications temps réel

# app/models/notification_broadcaster.rb
class NotificationBroadcaster
CHANNEL = "notifications"
def self.broadcast(user_id, message)
REDIS.publish(CHANNEL, {
user_id: user_id,
message: message,
timestamp: Time.current
}.to_json)
end
def self.subscribe
REDIS.subscribe(CHANNEL) do |on|
on.message do |channel, message|
data = JSON.parse(message)
# Traiter le message
ActionCable.server.broadcast(
"user:#{data['user_id']}",
notification: data['message']
)
end
end
end
end
# Utilisation
NotificationBroadcaster.broadcast(user.id, "Nouveau message reçu!")

Pièges à éviter

1. Oublier que tout est en mémoire

Problème : Stocker trop de données sans expiration consomme toute la RAM.

Solution : Toujours définir des TTL et monitorer l'utilisation mémoire.

# ❌ Dangereux
1_000_000.times do |i|
REDIS.set("item:#{i}", large_data) # Pas d'expiration !
end
# ✅ Sûr
1_000_000.times do |i|
REDIS.setex("item:#{i}", 1.hour.to_i, large_data)
end

2. Utiliser Redis comme base de données principale

Problème : Redis n'est pas conçu pour remplacer PostgreSQL. La persistance n'est pas sa force principale.

Solution : Utilisez Redis comme complément, pas comme remplacement.

# ❌ Risqué
REDIS.set("user:#{id}:email", user.email) # Donnée critique dans Redis
# ✅ Correct
user = User.find(id) # PostgreSQL = source de vérité
REDIS.setex("user:#{id}:cache", 5.minutes, user.to_json) # Redis = cache

3. Ignorer les opérations O(N)

Problème : Certaines commandes sont lentes sur de grandes collections.

Commandes à éviter sur grandes données :

  1. KEYS * (utiliser SCAN à la place)
  2. SMEMBERS sur de gros sets (utiliser SSCAN)
  3. HGETALL sur de gros hashes (utiliser HSCAN)
# ❌ Bloque Redis sur 1M+ de clés
all_keys = REDIS.keys("user:*")
# ✅ Itère par batch
REDIS.scan_each(match: "user:*", count: 100) do |key|
# Traiter chaque clé
end

4. Ne pas gérer la sérialisation

Problème : Redis stocke des strings. Vous devez sérialiser les objets complexes.

# ❌ Ne fonctionne pas
REDIS.set("user:data", { name: "Alice", age: 25 })
# => Erreur ou résultat inattendu
# ✅ Sérialiser en JSON
REDIS.set("user:data", { name: "Alice", age: 25 }.to_json)
data = JSON.parse(REDIS.get("user:data"))
# ✅ Ou utiliser Marshal (attention à la compatibilité)
REDIS.set("user:data", Marshal.dump(object))
object = Marshal.load(REDIS.get("user:data"))

5. Ignorer la sécurité

Problème : Redis par défaut n'a pas d'authentification et écoute sur toutes les interfaces.

Solution en production :

# /etc/redis/redis.conf
bind 127.0.0.1 # Écouter uniquement en local
requirepass votre_mot_de_passe_fort
# config/initializers/redis.rb
REDIS = Redis.new(
host: ENV['REDIS_HOST'],
port: ENV['REDIS_PORT'],
password: ENV['REDIS_PASSWORD'], # Important !
ssl: Rails.env.production? # TLS en production
)

Optimisation SEO et performance

Impact sur le SEO

Redis améliore indirectement votre SEO en accélérant votre site :

  1. Vitesse de chargement : Google favorise les sites rapides
  2. Temps de réponse réduit = meilleur classement
  3. Cache de fragments HTML pour pages instantanées
  4. Taux de rebond : Site plus rapide = utilisateurs restent plus longtemps
  5. Moins de frustration
  6. Plus d'engagement
  7. Core Web Vitals : Redis aide à améliorer les métriques clés
  8. LCP (Largest Contentful Paint) : cache des images et contenus
  9. FID (First Input Delay) : réponse plus rapide
  10. CLS (Cumulative Layout Shift) : chargement prévisible

Exemple de cache pour SEO

# Cache complet de pages pour SEO
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
# Cache HTML complet pour les robots et visiteurs non connectés
if !user_signed_in? && !request.bot?
fresh_when(etag: @article, last_modified: @article.updated_at)
end
end
end
# Cache fragment pour parties coûteuses
<% cache ["article", @article, "sidebar"], expires_in: 1.hour do %>
<%= render "articles/sidebar", article: @article %>
<% end %>

Conclusion : Redis, votre nouvel allié performance

Nous avons fait un tour complet de Redis, de ses concepts fondamentaux à son intégration dans Rails 8. Récapitulons ce que vous devez retenir :


Ce qu'est Redis :

  1. Une base de données clé-valeur en mémoire ultra-rapide
  2. Un outil polyvalent avec des structures de données riches
  3. Un complément essentiel aux bases de données traditionnelles


Quand utiliser Redis :

  1. ✅ Caching de données coûteuses
  2. ✅ Sessions utilisateur
  3. ✅ Compteurs et statistiques temps réel
  4. ✅ Leaderboards et classements
  5. ✅ Files d'attente de jobs (Sidekiq)
  6. ✅ Rate limiting
  7. ✅ Pub/Sub pour WebSockets


L'écosystème Rails 8 :

  1. Solid Cache par défaut pour simplifier le déploiement
  2. Redis toujours pertinent pour les cas spécifiques
  3. Approche hybride recommandée


Les bonnes pratiques :

  1. Nommez vos clés de manière structurée
  2. Définissez toujours des expirations
  3. Utilisez les structures de données appropriées
  4. Gérez les erreurs gracieusement
  5. Monitorez l'utilisation mémoire


Redis n'est pas magique, mais c'est un outil puissant qui, bien utilisé, transformera les performances de votre application Rails. Commencez petit : ajoutez du cache sur une requête lente, implémentez un compteur simple, puis explorez progressivement les autres possibilités.


Prochaines étapes :

  1. Installez Redis localement et testez les commandes de base
  2. Ajoutez le caching Redis sur une page lente de votre app
  3. Explorez Sidekiq pour les jobs asynchrones
  4. Implémentez un leaderboard ou un système de rate limiting


Vous avez maintenant toutes les clés (sans jeu de mots 😉) pour maîtriser Redis dans vos projets Rails. Bon coding !

Ressources pour aller plus loin

  1. Documentation officielle Redis
  2. Redis University - Cours gratuits
  3. Rails Guides - Caching
  4. Solid Cache GitHub
  5. Redis Command Reference
  6. Sidekiq Documentation