Développement Web

Pub/Sub temps réel avec Redis 8 : notifications et chat

9 دقائق للقراءة

📌 Article principal de la série : Redis 8 : caching, queues, pub/sub et streams pour applications production

Le mécanisme publish/subscribe de Redis permet la diffusion en temps réel d’événements entre plusieurs processus. Un publieur envoie un message sur un canal nommé ; tous les abonnés à ce canal reçoivent instantanément le message. Ce tutoriel construit une application chat temps réel avec WebSocket + Socket.IO + Redis pub/sub, capable de scaler horizontalement sur plusieurs serveurs Node.js.

Prérequis

  • Redis 8 opérationnel
  • Node.js 22 LTS
  • Bases async/await et WebSocket
  • Temps estimé : 60 minutes

Étape 1 — Comprendre le modèle pub/sub Redis

Trois commandes constituent l’API pub/sub : SUBSCRIBE canal1 [canal2 ...] abonne un client à un ou plusieurs canaux ; PUBLISH canal message envoie un message ; PSUBSCRIBE pattern abonne à un pattern (avec wildcards). À noter : un client connecté en mode subscribe ne peut plus exécuter de commandes normales sur cette connexion — d’où la nécessité d’avoir deux connexions Redis dans une application.

# Terminal 1 : abonné
redis-cli --user appuser -a "$PWD" SUBSCRIBE notifications

# Terminal 2 : publieur
redis-cli --user appuser -a "$PWD" PUBLISH notifications '{"user":"aissa","action":"login"}'

Le terminal 1 affiche immédiatement le message reçu. Cette simplicité radicale est la force du pub/sub Redis : pas de configuration, pas de persistance, pas de routage complexe — juste un broadcast léger. La latence sur un réseau local est sub-milliseconde.

Étape 2 — Application Node.js avec Socket.IO

Socket.IO est le framework WebSocket de référence côté Node.js. Couplé à Redis pub/sub via son adapter @socket.io/redis-adapter, il devient horizontalement scalable : peu importe sur quel serveur Node.js un utilisateur se connecte, ses messages atteindront tous les autres utilisateurs même connectés à des serveurs différents.

mkdir chat-redis-demo
cd chat-redis-demo
npm init -y
npm install express socket.io @socket.io/redis-adapter ioredis

Cette installation prépare un serveur Express avec Socket.IO et l’adapter Redis qui synchronise les rooms et les broadcasts à travers plusieurs instances de Node.js.

Étape 3 — Code du serveur chat

// server.js
import { createServer } from 'http';
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'ioredis';
import express from 'express';

const app = express();
app.use(express.static('public'));
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });

const pubClient = new (await import('ioredis')).default({
    host: '127.0.0.1', port: 6379,
    username: 'appuser', password: process.env.REDIS_PASSWORD
});
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

io.on('connection', (socket) => {
    console.log(`Client ${socket.id} connecte`);

    socket.on('rejoindre-salle', (salle) => {
        socket.join(salle);
        socket.to(salle).emit('utilisateur-rejoint', { socketId: socket.id, salle });
    });

    socket.on('message', ({ salle, contenu }) => {
        const payload = { de: socket.id, contenu, ts: Date.now() };
        io.to(salle).emit('message', payload);
    });

    socket.on('disconnect', () => {
        console.log(`Client ${socket.id} deconnecte`);
    });
});

httpServer.listen(3000, () => console.log('Chat sur http://localhost:3000'));

Deux connexions Redis sont créées : pubClient pour publier (et envoyer commandes normales) et subClient dupliqué pour s’abonner aux canaux internes utilisés par l’adapter. L’adapter Socket.IO réécrit transparentement les io.to(room).emit() en PUBLISH sur Redis, qui sont reçus par toutes les instances Socket.IO connectées au même Redis. Lancez plusieurs instances avec PM2 ou Docker Compose : les utilisateurs sur l’instance A recevront les messages des utilisateurs sur l’instance B.

Étape 4 — Client web minimal

<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Chat Redis</title></head>
<body>
    <h1>Chat temps reel</h1>
    <input id="salle" placeholder="Nom de la salle" value="general">
    <button id="rejoindre">Rejoindre</button>
    <ul id="messages"></ul>
    <input id="msg" placeholder="Votre message...">
    <button id="envoyer">Envoyer</button>

    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io();
        let salleActuelle = null;

        document.getElementById('rejoindre').onclick = () => {
            salleActuelle = document.getElementById('salle').value;
            socket.emit('rejoindre-salle', salleActuelle);
        };

        document.getElementById('envoyer').onclick = () => {
            const contenu = document.getElementById('msg').value;
            socket.emit('message', { salle: salleActuelle, contenu });
            document.getElementById('msg').value = '';
        };

        socket.on('message', (m) => {
            const li = document.createElement('li');
            li.textContent = `${m.de.slice(0,4)}: ${m.contenu}`;
            document.getElementById('messages').appendChild(li);
        });

        socket.on('utilisateur-rejoint', (u) => {
            const li = document.createElement('li');
            li.style.color = '#888';
            li.textContent = `${u.socketId.slice(0,4)} a rejoint`;
            document.getElementById('messages').appendChild(li);
        });
    </script>
</body></html>

Le client se connecte à Socket.IO via io() (qui résout automatiquement l’URL du serveur). Il rejoint une salle nommée, envoie des messages, et reçoit les broadcasts. Pour tester la scalabilité horizontale, lancez deux instances du serveur sur les ports 3000 et 3001 derrière un load balancer simple (caddy, nginx) et vérifiez que les messages se propagent.

Étape 5 — Pattern fan-out pour notifications

Au-delà du chat, le pub/sub sert massivement à propager des notifications inter-services. Un service de commande publie un événement, un service d’email s’y abonne pour envoyer une confirmation, un service de stock s’y abonne aussi pour décrémenter l’inventaire.

// service-commande.js
async function creerCommande(commande) {
    // ... logique business ...
    await pubClient.publish('commandes:creees', JSON.stringify({
        id: commande.id,
        userId: commande.userId,
        total: commande.total,
        ts: Date.now()
    }));
}

// service-email.js (processus separe)
const sub = new Redis(/* config */);
sub.subscribe('commandes:creees');
sub.on('message', async (canal, payload) => {
    const cmd = JSON.parse(payload);
    await envoyerEmailConfirmation(cmd.userId, cmd.id);
});

// service-stock.js (autre processus)
const sub2 = new Redis(/* config */);
sub2.subscribe('commandes:creees');
sub2.on('message', async (canal, payload) => {
    const cmd = JSON.parse(payload);
    await decrementerStock(cmd.id);
});

Cette architecture découple les services : ajouter un nouveau consommateur (analytics, ML, notifications mobile) ne nécessite aucun changement dans le service publieur. Limitation : si le service email est down au moment du PUBLISH, le message est perdu. Pour les événements business critiques, préférer Redis Streams (voir le tutoriel suivant).

Étape 6 — PSUBSCRIBE pour patterns

La commande PSUBSCRIBE s’abonne à un pattern avec wildcards plutôt qu’à un canal fixe. C’est utile pour des architectures par tenants ou par régions.

// Abonnement a tous les canaux d'une region
sub.psubscribe('region:dakar:*');

sub.on('pmessage', (pattern, canal, msg) => {
    console.log(`Recu sur ${canal} : ${msg}`);
});

// Le publisher publie sur des canaux specifiques :
pub.publish('region:dakar:commandes', JSON.stringify(c));
pub.publish('region:dakar:livraisons', JSON.stringify(l));
pub.publish('region:abidjan:commandes', JSON.stringify(c2));

L’abonné via pattern region:dakar:* reçoit les deux premiers messages mais pas le troisième (Abidjan). Les patterns supportent * (zéro ou plus de caractères), ? (un seul caractère) et [abc] (un caractère parmi). Attention : PSUBSCRIBE est légèrement plus coûteux que SUBSCRIBE car Redis doit matcher chaque PUBLISH contre tous les patterns enregistrés.

Étape 7 — Sentinel et adapter

Pour la production, votre Redis devrait être en Sentinel HA. L’adapter Socket.IO et ioredis supportent nativement la configuration Sentinel.

const pubClient = new Redis({
    sentinels: [
        { host: 'redis-sentinel-1', port: 26379 },
        { host: 'redis-sentinel-2', port: 26379 },
        { host: 'redis-sentinel-3', port: 26379 }
    ],
    name: 'mymaster',
    username: 'appuser',
    password: process.env.REDIS_PASSWORD
});

En cas de failover (maître Redis qui tombe), Sentinel élit un nouveau maître et ioredis reconnecte automatiquement à la nouvelle adresse. Aucun changement de code applicatif n’est nécessaire. Voir le tutoriel Redis Sentinel et Cluster pour la configuration complète côté Redis.

Étape 8 — Limites du pub/sub Redis

Trois limites importantes à connaître. Premièrement, les messages ne sont pas persistés : si aucun abonné n’écoute au moment du PUBLISH, le message est perdu. Deuxièmement, il n’y a pas de delivery guarantee — un client lent dont le buffer déborde peut être déconnecté par Redis avec perte de messages. Troisièmement, l’ordre est garanti par canal mais pas entre canaux. Pour des cas d’usage qui demandent persistance, retry, ou ordering strict, utilisez Redis Streams.

Le pub/sub reste optimal pour : notifications UI temps réel, broadcasts éphémères (présence utilisateur, frappes en cours, statuts ligne), invalidation de cache distribué, et propagation d’événements à des consommateurs idempotents qui peuvent tolérer la perte occasionnelle d’un événement.

Erreurs fréquentes

Erreur Cause Solution
Cannot execute command while subscribed Une seule connexion utilisée pour subscribe et commandes normales Créer deux connexions distinctes via client.duplicate()
Messages perdus aléatoirement Client lent, buffer rempli, déconnexion forcée Augmenter client-output-buffer-limit pubsub dans redis.conf
Messages dupliqués chez subscribers Subscribe en boucle suite à reconnexion non gérée S’abonner une seule fois, écouter 'ready' pour resubscribe explicite après reconnect
Latence élevée sous charge Saturation CPU mono-thread Redis Passer en mode Cluster, ou utiliser plusieurs Redis dédiés par canal

Tutoriels suivants

FAQ

Pub/Sub Redis ou WebSocket pur ?
WebSocket gère la connexion navigateur↔serveur. Redis pub/sub gère le broadcast inter-serveurs Node.js. Pour une seule instance Node.js, WebSocket seul suffit. Dès qu’on scale horizontalement, Redis pub/sub devient nécessaire pour synchroniser les broadcasts.
Combien de subscribers maximum ?
Redis gère sans difficulté plusieurs dizaines de milliers de subscribers simultanés sur un même canal. La limite pratique est la bande passante réseau (chaque PUBLISH est répliqué vers chaque subscriber). Pour 100 000+ utilisateurs, envisager une architecture pub/sub hiérarchique avec un cluster Redis dédié et plusieurs frontaux Socket.IO.
Comment authentifier les WebSocket ?
Passer un JWT en query string lors du io(), le valider côté serveur dans le handler connection, et stocker l’identité dans socket.data. Ne jamais faire confiance aux données envoyées par le client sans validation côté serveur.

Références

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité