---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# NoSQL : Redis

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.gridspec as gridspec
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

Redis (*Remote Dictionary Server*) est une base de données en mémoire, open-source, créée par Salvatore Sanfilippo en 2009. Contrairement aux bases relationnelles qui stockent les données sur disque et les chargent à la demande, Redis maintient **tout en RAM**, ce qui lui permet d'atteindre des latences inférieures à la milliseconde sur des millions d'opérations par seconde. Cette caractéristique en fait l'outil de référence pour les cas d'usage où la vitesse est critique : cache, sessions, files de messages, classements en temps réel.

Redis n'est pas une base généraliste. On l'utilise à côté d'une base relationnelle — PostgreSQL ou MySQL — pour y déporter les données chaudes, les données temporaires ou les structures de données dont la manipulation en SQL serait coûteuse ou peu naturelle. Comprendre Redis, c'est comprendre quand **ne pas utiliser SQL**.

## Structures de données natives

La caractéristique la plus distinctive de Redis est d'exposer directement des **structures de données de haut niveau**, plutôt qu'un simple stockage clé-valeur brut. Chaque clé Redis est associée à un type, et les opérations disponibles dépendent du type.

```{prf:definition}
:label: ch16-def-structures-redis

Redis propose sept structures de données principales :

- **String** : valeur binaire arbitraire, jusqu'à 512 Mo. Utilisée pour les compteurs, les caches JSON, les flags.
- **List** : liste doublement liée de strings. Insertion en O(1) en tête ou en queue. Utilisée pour les files et les piles.
- **Hash** : dictionnaire de paires champ-valeur. Analogue à un objet ou à une ligne de table.
- **Set** : ensemble non ordonné de strings uniques. Opérations ensemblistes (union, intersection, différence).
- **Sorted Set** (ZSet) : ensemble ordonné par un score flottant. Parfait pour les classements.
- **Stream** : journal de messages immuables avec groupes de consommateurs.
- **Bitmap / HyperLogLog** : structures spécialisées pour le comptage et les statistiques approximatives.
```

```{prf:remark}
:label: ch16-rem-cle-convention

La convention habituelle pour nommer les clés Redis est `type:id:champ`, par exemple `user:42:session` ou `product:123:views`. Ce nommage structuré facilite la lisibilité et permet des recherches par pattern avec `SCAN`. Redis ne supporte pas de requêtes complexes sur les clés — la structure doit être encodée dans le nom.
```

## Commandes fondamentales

### String : SET, GET, INCR

Les Strings sont le type le plus simple et le plus polyvalent. Ils servent aussi bien à stocker une valeur JSON qu'à implémenter un compteur atomique.

```redis
SET compteur:visites 0
INCR compteur:visites          -- atomique : renvoie 1
INCRBY compteur:visites 10     -- renvoie 11
GET compteur:visites            -- renvoie "11"

SET cache:user:42 '{"nom":"Alice","role":"admin"}'
GET cache:user:42
```

```{prf:example}
:label: ch16-ex-string

Stocker et récupérer le nombre de pages vues d'un article de blog :

Chaque requête HTTP incrémente la clé `article:101:views` avec `INCR`. L'opération est atomique — même sous charge concurrente, Redis garantit qu'aucun incrément n'est perdu. Pour lire le total, une seule commande `GET` suffit. La latence est inférieure à 0,1 ms, contre plusieurs millisecondes pour une requête `UPDATE ... SET views = views + 1` en SQL sous charge.
```

### List : LPUSH, RPUSH, LRANGE, LPOP

Les Lists Redis sont des listes doublement liées. L'insertion en tête (`LPUSH`) ou en queue (`RPUSH`) est en O(1).

```redis
LPUSH notifications:user:42 "Nouveau message de Bob"
LPUSH notifications:user:42 "Votre commande est expédiée"
LRANGE notifications:user:42 0 -1    -- toute la liste (du plus récent au plus ancien)
RPOP notifications:user:42           -- consomme l'élément le plus ancien
LLEN notifications:user:42           -- longueur de la liste
```

```{prf:definition}
:label: ch16-def-pattern-queue

Le pattern **queue (FIFO)** avec Redis s'implémente en combinant `RPUSH` (enqueue) et `LPOP` (dequeue). Pour un traitement asynchrone sans perte de messages, on préfère `BLPOP` — la version bloquante — qui attend qu'un élément soit disponible plutôt que de retourner `nil`.
```

### Hash : HSET, HGET, HGETALL

Les Hash permettent de stocker un objet structuré sans avoir à sérialiser/désérialiser un JSON entier à chaque accès.

```redis
HSET user:42 nom "Alice" email "alice@example.com" role "admin" points 1500
HGET user:42 nom          -- "Alice"
HGETALL user:42           -- tous les champs
HINCRBY user:42 points 50 -- incrémenter un champ numérique
HDEL user:42 role          -- supprimer un champ
HEXISTS user:42 email      -- 1 (vrai)
```

```{prf:remark}
:label: ch16-rem-hash-vs-string

Stocker un objet dans un **Hash** plutôt que dans un **String JSON** a deux avantages : on peut lire ou modifier un seul champ sans toucher au reste (réduisant la bande passante), et Redis peut compresser les petits Hash en mémoire grâce à un encodage interne optimisé (ziplist). En pratique, pour des objets comportant moins d'une centaine de champs, les Hash sont plus économiques en mémoire que le String JSON.
```

### Set : SADD, SMEMBERS, SINTER

Les Set sont des ensembles sans doublon. Redis expose les opérations ensemblistes en O(N).

```redis
SADD tags:article:1 "python" "sql" "database"
SADD tags:article:2 "python" "nosql" "redis"
SMEMBERS tags:article:1          -- {"python", "sql", "database"}
SINTER tags:article:1 tags:article:2   -- intersection : {"python"}
SUNION tags:article:1 tags:article:2   -- union
SDIFF  tags:article:1 tags:article:2   -- différence
SISMEMBER tags:article:1 "python"      -- 1
```

### Sorted Set : ZADD, ZRANGE, ZRANK

Les Sorted Sets associent un score flottant à chaque membre. Redis maintient l'ordre automatiquement.

```redis
ZADD leaderboard 9800 "Alice"
ZADD leaderboard 9200 "Bob"
ZADD leaderboard 9950 "Clara"
ZRANGE leaderboard 0 -1 WITHSCORES REV    -- classement décroissant
ZRANK leaderboard "Bob"                    -- rang de Bob (0-based)
ZINCRBY leaderboard 100 "Bob"              -- Bob gagne 100 points
ZRANGEBYSCORE leaderboard 9000 10000       -- membres entre 9000 et 10000
```

```{prf:definition}
:label: ch16-def-sorted-set

Un **Sorted Set** Redis est implémenté en interne par deux structures complémentaires : une table de hachage (accès O(1) par membre) et un **skip list** (liste chaînée probabiliste à plusieurs niveaux permettant les opérations de rang en O(log N)). Cette dualité explique pourquoi les opérations par membre sont en O(1) alors que les opérations par plage de score sont en O(log N + K) où K est le nombre d'éléments retournés.
```

## TTL et expiration

Redis gère nativement l'expiration des clés, ce qui évite d'implémenter une logique de nettoyage applicative.

```redis
SET session:abc123 '{"user_id":42}' EX 3600    -- expire dans 3600 secondes
EXPIRE session:abc123 1800                      -- modifier le TTL
TTL session:abc123                              -- secondes restantes (-1 = jamais, -2 = inexistante)
PERSIST session:abc123                          -- supprimer le TTL (rendre permanente)
PTTL session:abc123                             -- TTL en millisecondes
```

```{prf:definition}
:label: ch16-def-ttl

Le **TTL** (*Time To Live*) est le nombre de secondes avant qu'une clé soit automatiquement supprimée par Redis. Redis utilise deux stratégies complémentaires : la suppression **lazy** (la clé est vérifiée lors de son prochain accès) et la suppression **active** (un processus en arrière-plan sample aléatoirement des clés expirées à intervalle régulier). Ces deux mécanismes garantissent que la mémoire est récupérée sans surcharge significative.
```

```{prf:example}
:label: ch16-ex-session

Gestion de sessions HTTP avec Redis :

À la connexion, on crée une clé `session:{token}` de type Hash contenant l'identifiant utilisateur, le rôle et le timestamp. On lui donne un TTL de 30 minutes avec `EXPIRE`. À chaque requête authentifiée, on appelle `HGETALL session:{token}` (< 0,1 ms) et on réinitialise le TTL avec `EXPIRE`. Si la session expire sans activité, Redis la supprime automatiquement — pas de cron de nettoyage à écrire.
```

## Pub/Sub : SUBSCRIBE, PUBLISH

Redis intègre un mécanisme de messagerie **publish/subscribe** permettant à des processus de communiquer de manière découplée.

```redis
-- Terminal 1 (subscriber)
SUBSCRIBE notifications

-- Terminal 2 (publisher)
PUBLISH notifications "Nouveau commentaire sur votre article"
PUBLISH notifications "Votre paiement a été accepté"
```

```{prf:definition}
:label: ch16-def-pubsub

Dans le modèle **pub/sub**, les **publishers** envoient des messages sur des **canaux** sans connaître les destinataires. Les **subscribers** s'abonnent aux canaux qui les intéressent et reçoivent tous les messages publiés. Redis supporte les abonnements par pattern avec `PSUBSCRIBE notif:*` (wildcard). Ce modèle est **fire-and-forget** : si un subscriber n'est pas connecté au moment de la publication, le message est perdu. Pour la persistance, on utilise les **Streams**.
```

```{prf:remark}
:label: ch16-rem-pubsub-limitations

Les limitations du pub/sub Redis : (1) pas de persistance — les messages publiés quand aucun subscriber n'écoute sont perdus ; (2) pas de groupes de consommateurs — tous les subscribers d'un canal reçoivent tous les messages (pas de partage de charge) ; (3) pas d'acquittement. Pour des cas d'usage plus robustes, Redis Streams (`XADD`, `XREAD`, `XGROUP`) offrent la persistance et les groupes de consommateurs.
```

## Persistance : RDB et AOF

Bien que Redis soit une base en mémoire, il offre deux mécanismes de persistance sur disque pour survivre aux redémarrages.

```{prf:definition}
:label: ch16-def-rdb

**RDB (Redis Database)** est le mécanisme de snapshot. Redis prend périodiquement une photographie de l'état en mémoire et l'écrit dans un fichier binaire compressé `dump.rdb`. La configuration typique déclenche un snapshot si au moins 1 modification a eu lieu en 900 secondes, ou 10 modifications en 300 secondes. La restauration est rapide, mais les données entre le dernier snapshot et le crash sont perdues.
```

```{prf:definition}
:label: ch16-def-aof

**AOF (Append-Only File)** est le mécanisme de journal. Chaque commande d'écriture est ajoutée au fichier `appendonly.aof`. Au redémarrage, Redis rejoue le journal. La politique de synchronisation est configurable : `always` (sync à chaque écriture, durabilité maximale, moins rapide), `everysec` (sync par seconde, compromis recommandé), ou `no` (laissé au système d'exploitation). L'AOF grossit au fil du temps ; Redis le compacte automatiquement via la **réécriture AOF** (`BGREWRITEAOF`).
```

```{prf:remark}
:label: ch16-rem-persistance-hybride

En production, on peut activer les deux modes simultanément : RDB pour les backups rapides et la restauration rapide, AOF pour la durabilité fine. Redis 7 introduit le format **RDB+AOF** qui combine les avantages des deux. Sans persistance (`--save ""`), Redis est un cache pur : si le processus redémarre, tout est perdu — ce qui est acceptable si la base de données principale peut repeupler le cache.
```

## Cellule exécutable : simulation des structures Redis

```{code-cell} python
import bisect
import time
from collections import defaultdict

class RedisSimulator:
    """Simulation Python des principales structures de données Redis."""

    def __init__(self):
        self._store = {}      # clé -> valeur (str, list, dict, set)
        self._types = {}      # clé -> type
        self._ttls = {}       # clé -> timestamp d'expiration

    def _check_expired(self, key):
        if key in self._ttls and time.time() > self._ttls[key]:
            del self._store[key]
            del self._types[key]
            del self._ttls[key]
            return True
        return False

    # --- String ---
    def set(self, key, value, ex=None):
        self._store[key] = str(value)
        self._types[key] = 'string'
        if ex:
            self._ttls[key] = time.time() + ex
        elif key in self._ttls:
            del self._ttls[key]

    def get(self, key):
        if self._check_expired(key):
            return None
        return self._store.get(key)

    def incr(self, key):
        val = int(self._store.get(key, 0)) + 1
        self.set(key, val)
        return val

    # --- Hash ---
    def hset(self, key, mapping):
        if key not in self._store or self._types[key] != 'hash':
            self._store[key] = {}
            self._types[key] = 'hash'
        self._store[key].update(mapping)

    def hget(self, key, field):
        if self._check_expired(key):
            return None
        return self._store.get(key, {}).get(field)

    def hgetall(self, key):
        if self._check_expired(key):
            return {}
        return dict(self._store.get(key, {}))

    def hincrby(self, key, field, amount):
        if key not in self._store:
            self._store[key] = {}
            self._types[key] = 'hash'
        self._store[key][field] = int(self._store[key].get(field, 0)) + amount

    # --- List ---
    def lpush(self, key, *values):
        if key not in self._store or self._types[key] != 'list':
            self._store[key] = []
            self._types[key] = 'list'
        for v in values:
            self._store[key].insert(0, v)
        return len(self._store[key])

    def rpush(self, key, *values):
        if key not in self._store or self._types[key] != 'list':
            self._store[key] = []
            self._types[key] = 'list'
        self._store[key].extend(values)
        return len(self._store[key])

    def lrange(self, key, start, stop):
        lst = self._store.get(key, [])
        if stop == -1:
            return list(lst[start:])
        return list(lst[start:stop+1])

    def lpop(self, key):
        lst = self._store.get(key, [])
        if lst:
            return lst.pop(0)
        return None

    # --- Set ---
    def sadd(self, key, *members):
        if key not in self._store or self._types[key] != 'set':
            self._store[key] = set()
            self._types[key] = 'set'
        before = len(self._store[key])
        self._store[key].update(members)
        return len(self._store[key]) - before

    def smembers(self, key):
        return set(self._store.get(key, set()))

    def sinter(self, *keys):
        sets = [self._store.get(k, set()) for k in keys]
        return set.intersection(*sets) if sets else set()

    # --- Sorted Set ---
    def zadd(self, key, mapping):
        if key not in self._store or self._types[key] != 'zset':
            self._store[key] = {}
            self._types[key] = 'zset'
        self._store[key].update(mapping)

    def zrange(self, key, start=0, stop=-1, reverse=False):
        zset = self._store.get(key, {})
        ranked = sorted(zset.items(), key=lambda x: x[1], reverse=reverse)
        if stop == -1:
            return ranked[start:]
        return ranked[start:stop+1]

    def zincrby(self, key, amount, member):
        if key not in self._store:
            self._store[key] = {}
            self._types[key] = 'zset'
        self._store[key][member] = self._store[key].get(member, 0) + amount
        return self._store[key][member]

    # --- TTL ---
    def expire(self, key, seconds):
        if key in self._store:
            self._ttls[key] = time.time() + seconds
            return 1
        return 0

    def ttl(self, key):
        if key not in self._store:
            return -2
        if key not in self._ttls:
            return -1
        remaining = self._ttls[key] - time.time()
        return max(0, int(remaining))

# --- Démonstration ---
r = RedisSimulator()

# String + compteur
r.set("article:101:views", 0)
for _ in range(15):
    r.incr("article:101:views")
print(f"Vues article 101 : {r.get('article:101:views')}")

# Hash (profil utilisateur)
r.hset("user:42", {"nom": "Alice", "email": "alice@example.com", "points": "1500"})
r.hincrby("user:42", "points", 250)
print(f"Profil user:42 : {r.hgetall('user:42')}")

# List (file de notifications)
r.rpush("notif:42", "Message de Bob", "Commande expédiée", "Nouveau follower")
print(f"Notifications : {r.lrange('notif:42', 0, -1)}")
print(f"Pop : {r.lpop('notif:42')}")

# Set (tags)
r.sadd("tags:article:1", "python", "sql", "database")
r.sadd("tags:article:2", "python", "nosql", "redis")
print(f"Intersection tags : {r.sinter('tags:article:1', 'tags:article:2')}")

# Sorted Set (leaderboard)
r.zadd("leaderboard", {"Alice": 9800, "Bob": 9200, "Clara": 9950, "David": 8700})
r.zincrby("leaderboard", 150, "Bob")
print("\nClassement (top 3) :")
for i, (name, score) in enumerate(r.zrange("leaderboard", reverse=True)[:3], 1):
    print(f"  {i}. {name} — {score:.0f} pts")

# TTL
r.set("session:xyz", "user=42", ex=1800)
print(f"\nTTL session : {r.ttl('session:xyz')} secondes restantes")
```

## Visualisation des structures

```{code-cell} python
:tags: [hide-input]

fig = plt.figure(figsize=(14, 9))
gs = gridspec.GridSpec(2, 3, figure=fig, hspace=0.55, wspace=0.45)

colors = sns.color_palette("muted", 6)

# --- String ---
ax0 = fig.add_subplot(gs[0, 0])
ax0.set_title("String", fontweight="bold", fontsize=11)
ax0.set_xlim(0, 4); ax0.set_ylim(0, 3); ax0.axis("off")
rect = patches.FancyBboxPatch((0.3, 0.8), 3.2, 1.2, boxstyle="round,pad=0.1",
                               fc=colors[0], alpha=0.7, ec="none")
ax0.add_patch(rect)
ax0.text(1.9, 1.4, '"9hZk...abc"', ha='center', va='center', fontsize=9, color='white', fontweight='bold')
ax0.text(0.7, 2.4, 'clé', fontsize=8, color='gray')
ax0.text(2.5, 2.4, 'valeur (jusqu\'à 512 Mo)', fontsize=8, color='gray')
ax0.annotate('', xy=(1.9, 2.1), xytext=(1.9, 2.35),
             arrowprops=dict(arrowstyle='->', color='gray'))

# --- List ---
ax1 = fig.add_subplot(gs[0, 1])
ax1.set_title("List", fontweight="bold", fontsize=11)
ax1.set_xlim(0, 5); ax1.set_ylim(0, 3); ax1.axis("off")
elements = ["A", "B", "C", "D"]
for i, e in enumerate(elements):
    rect = patches.FancyBboxPatch((0.1 + i*1.1, 0.8), 0.9, 1.2,
                                   boxstyle="round,pad=0.05", fc=colors[1], alpha=0.7, ec="none")
    ax1.add_patch(rect)
    ax1.text(0.55 + i*1.1, 1.4, e, ha='center', va='center', fontsize=10, color='white', fontweight='bold')
    if i < 3:
        ax1.annotate('', xy=(1.0 + i*1.1, 1.4), xytext=(0.95 + (i+1)*1.1 - 0.9, 1.4),
                     arrowprops=dict(arrowstyle='<->', color='#555', lw=1.2))
ax1.text(0.35, 2.4, 'LPUSH', fontsize=7.5, color=colors[1])
ax1.text(3.7, 2.4, 'RPUSH', fontsize=7.5, color=colors[1], ha='right')

# --- Hash ---
ax2 = fig.add_subplot(gs[0, 2])
ax2.set_title("Hash", fontweight="bold", fontsize=11)
ax2.set_xlim(0, 4); ax2.set_ylim(0, 3); ax2.axis("off")
fields = [("nom", "Alice"), ("email", "alice@…"), ("points", "1750")]
for i, (k, v) in enumerate(fields):
    y = 2.2 - i * 0.65
    rect_k = patches.FancyBboxPatch((0.1, y - 0.22), 1.3, 0.42, boxstyle="round,pad=0.05",
                                     fc=colors[2], alpha=0.6, ec="none")
    rect_v = patches.FancyBboxPatch((1.55, y - 0.22), 2.3, 0.42, boxstyle="round,pad=0.05",
                                     fc=colors[2], alpha=0.3, ec="none")
    ax2.add_patch(rect_k); ax2.add_patch(rect_v)
    ax2.text(0.75, y, k, ha='center', va='center', fontsize=8, color='white', fontweight='bold')
    ax2.text(2.7, y, v, ha='center', va='center', fontsize=8, color='#333')

# --- Set ---
ax3 = fig.add_subplot(gs[1, 0])
ax3.set_title("Set", fontweight="bold", fontsize=11)
ax3.set_xlim(0, 5); ax3.set_ylim(0, 3); ax3.axis("off")
members = ["python", "sql", "redis", "nosql"]
positions = [(1.0, 1.8), (2.8, 2.2), (1.6, 0.9), (3.4, 1.3)]
for (x, y), m in zip(positions, members):
    circ = plt.Circle((x, y), 0.5, fc=colors[3], alpha=0.6, ec="none")
    ax3.add_patch(circ)
    ax3.text(x, y, m, ha='center', va='center', fontsize=7.5, color='white', fontweight='bold')

# --- Sorted Set ---
ax4 = fig.add_subplot(gs[1, 1])
ax4.set_title("Sorted Set (leaderboard)", fontweight="bold", fontsize=11)
ax4.set_xlim(0, 5); ax4.set_ylim(0, 3); ax4.axis("off")
members_z = [("Clara", 9950), ("Alice", 9800), ("Bob", 9350)]
for i, (name, score) in enumerate(members_z):
    y = 2.3 - i * 0.7
    rect_s = patches.FancyBboxPatch((0.1, y - 0.22), 0.9, 0.42, boxstyle="round,pad=0.05",
                                     fc=colors[4], alpha=0.8, ec="none")
    rect_n = patches.FancyBboxPatch((1.1, y - 0.22), 2.8, 0.42, boxstyle="round,pad=0.05",
                                     fc=colors[4], alpha=0.35, ec="none")
    ax4.add_patch(rect_s); ax4.add_patch(rect_n)
    ax4.text(0.55, y, f"#{i+1}", ha='center', va='center', fontsize=8, color='white', fontweight='bold')
    ax4.text(2.5, y, f"{name} — {score}", ha='center', va='center', fontsize=8.5, color='#333')

# --- Stream ---
ax5 = fig.add_subplot(gs[1, 2])
ax5.set_title("Stream (journal)", fontweight="bold", fontsize=11)
ax5.set_xlim(0, 5); ax5.set_ylim(0, 3); ax5.axis("off")
msgs = ["1700000001-0", "1700000002-0", "1700000003-0"]
for i, msg_id in enumerate(msgs):
    y = 2.3 - i * 0.7
    rect = patches.FancyBboxPatch((0.1, y - 0.22), 4.6, 0.42, boxstyle="round,pad=0.05",
                                   fc=colors[5], alpha=0.5 + i*0.1, ec="none")
    ax5.add_patch(rect)
    ax5.text(1.5, y, msg_id, ha='center', va='center', fontsize=7.5, color='white')
    ax5.text(3.5, y, f"champ: val_{i+1}", ha='center', va='center', fontsize=7.5, color='#333')

fig.suptitle("Structures de données Redis", fontsize=14, fontweight='bold', y=1.01)
plt.savefig("redis_structures.png", bbox_inches="tight", dpi=120)
plt.show()
```

## Diagramme Pub/Sub

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(0, 12); ax.set_ylim(0, 6); ax.axis('off')
ax.set_title("Architecture Pub/Sub Redis", fontsize=13, fontweight='bold', pad=10)

colors_pubsub = sns.color_palette("muted", 4)

# Publisher
pub_box = patches.FancyBboxPatch((0.2, 2.2), 2.2, 1.6, boxstyle="round,pad=0.15",
                                   fc=colors_pubsub[0], alpha=0.8, ec="none")
ax.add_patch(pub_box)
ax.text(1.3, 3.1, "Publisher", ha='center', va='center', fontsize=11,
        color='white', fontweight='bold')
ax.text(1.3, 2.7, "PUBLISH canal msg", ha='center', va='center', fontsize=7.5, color='white')

# Canal Redis (au centre)
canal_box = patches.FancyBboxPatch((4.5, 1.5), 3.0, 3.0, boxstyle="round,pad=0.2",
                                    fc=colors_pubsub[2], alpha=0.7, ec="none")
ax.add_patch(canal_box)
ax.text(6.0, 3.5, "Redis", ha='center', va='center', fontsize=12,
        color='white', fontweight='bold')
ax.text(6.0, 3.0, "Canal : notifications", ha='center', va='center', fontsize=8.5, color='white')
ax.text(6.0, 2.5, "Canal : alertes", ha='center', va='center', fontsize=8.5, color='white')
ax.text(6.0, 2.0, "Canal : logs", ha='center', va='center', fontsize=8.5, color='white')

# Subscribers
for i, (y, label, sub) in enumerate([(4.2, "Sub A", "SUBSCRIBE notifications"),
                                       (3.0, "Sub B", "SUBSCRIBE notifications\nSUBSCRIBE alertes"),
                                       (1.6, "Sub C", "SUBSCRIBE alertes")]):
    sub_box = patches.FancyBboxPatch((9.5, y - 0.5), 2.3, 1.1, boxstyle="round,pad=0.1",
                                      fc=colors_pubsub[1], alpha=0.8, ec="none")
    ax.add_patch(sub_box)
    ax.text(10.65, y + 0.05, label, ha='center', va='center', fontsize=10,
            color='white', fontweight='bold')

# Flèches publisher -> canal
ax.annotate('', xy=(4.5, 3.0), xytext=(2.4, 3.0),
            arrowprops=dict(arrowstyle='->', color='#333', lw=2))

# Flèches canal -> subscribers
for y in [4.2, 3.0, 1.6]:
    ax.annotate('', xy=(9.5, y), xytext=(7.5, 3.0),
                arrowprops=dict(arrowstyle='->', color='#333', lw=1.5, connectionstyle='arc3,rad=0.1'))

ax.text(3.0, 3.25, "PUBLISH", fontsize=8, color='#555', style='italic')
ax.text(8.2, 4.0, "delivery", fontsize=7.5, color='#555', style='italic')

plt.show()
```

## Cas d'usage : Redis vs SQL

```{prf:example}
:label: ch16-ex-cache

**Cache applicatif** : lors d'une requête sur `/api/products/bestsellers`, l'application cherche d'abord la clé `cache:bestsellers` dans Redis (TTL 5 minutes). Si elle existe, on retourne directement la valeur JSON (0,1 ms). Sinon, on exécute la requête SQL coûteuse, on stocke le résultat dans Redis, et on le retourne. Ce pattern — appelé **cache-aside** — réduit la charge SQL de 80 à 95 % pour les endpoints populaires.
```

```{prf:example}
:label: ch16-ex-leaderboard

**Leaderboard en temps réel** : un Sorted Set Redis stocke les scores de tous les joueurs d'un jeu. Chaque partie incrémente le score avec `ZINCRBY`. L'affichage du top-100 se fait avec `ZRANGE leaderboard 0 99 WITHSCORES REV` en O(log N + 100). En SQL, une requête `SELECT ... ORDER BY score DESC LIMIT 100` sur une table de millions de joueurs nécessite un index et reste bien plus lente sous forte concurrence.
```

```{prf:example}
:label: ch16-ex-ratelimit

**Rate limiting** : pour limiter une API à 100 requêtes par minute par utilisateur, on utilise un compteur avec TTL. Lors de chaque requête, `INCR rate:user:42` incrémente le compteur. Si la valeur retournée vaut 1, on appelle `EXPIRE rate:user:42 60`. Si la valeur dépasse 100, on rejette la requête avec HTTP 429. L'implémentation tient en 3 lignes de code Redis et est atomique, sans risque de race condition.
```

```{code-cell} python
:tags: [hide-input]

# Comparaison Redis vs SQL pour différents cas d'usage
cas_usage = [
    "Cache\nrésultats",
    "Sessions\nutilisateurs",
    "Leaderboard\ntemps réel",
    "Rate\nlimiting",
    "Queue\nastucieux",
    "Recherche\ncomplexe",
    "Transactions\nACID",
    "Rapports\nBI"
]
redis_score = [10, 10, 10, 10, 9, 2, 3, 1]
sql_score   = [4,  3,  4,  3, 5, 10, 10, 10]

x = np.arange(len(cas_usage))
width = 0.38

fig, ax = plt.subplots(figsize=(13, 5))
bars1 = ax.bar(x - width/2, redis_score, width, label='Redis', color=colors[0], alpha=0.8)
bars2 = ax.bar(x + width/2, sql_score,   width, label='SQL (PostgreSQL)', color=colors[1], alpha=0.8)

ax.set_xticks(x)
ax.set_xticklabels(cas_usage, fontsize=9)
ax.set_ylabel("Adéquation (0–10)")
ax.set_title("Redis vs SQL : adéquation par cas d'usage", fontsize=13, fontweight='bold')
ax.legend()
ax.set_ylim(0, 12)
ax.yaxis.grid(True, alpha=0.4)
ax.set_axisbelow(True)
plt.show()
```

## Utilisation avec redis-py (blocs illustratifs)

Les exemples suivants nécessitent un serveur Redis actif (`redis-server`) et la bibliothèque `redis-py` (`pip install redis`).

```python
import redis

r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# String
r.set("compteur", 0)
r.incr("compteur")

# Hash
r.hset("user:42", mapping={"nom": "Alice", "points": 1500})
print(r.hgetall("user:42"))  # {'nom': 'Alice', 'points': '1500'}

# TTL
r.set("session:abc", "user=42", ex=3600)
print(r.ttl("session:abc"))  # ~3600

# Sorted Set
r.zadd("leaderboard", {"Alice": 9800, "Bob": 9200})
print(r.zrange("leaderboard", 0, -1, withscores=True, desc=True))
```

```python
# Pub/Sub avec redis-py (processus publisher)
import redis, json

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

message = json.dumps({"type": "commande", "id": 1024, "statut": "expédiée"})
abonnés = r.publish("notifications", message)
print(f"Message envoyé à {abonnés} subscriber(s)")
```

```python
# Pub/Sub avec redis-py (processus subscriber)
import redis, json

r = redis.Redis(host='localhost', port=6379, decode_responses=True)
pubsub = r.pubsub()
pubsub.subscribe("notifications")

for message in pubsub.listen():
    if message["type"] == "message":
        data = json.loads(message["data"])
        print(f"Reçu : {data}")
```

```{prf:remark}
:label: ch16-rem-pipeline

**Pipelines redis-py** : pour envoyer plusieurs commandes en un seul aller-retour réseau, on utilise `r.pipeline()`. Cela réduit drastiquement la latence cumulée quand on doit exécuter des dizaines de commandes en séquence. Les pipelines ne sont pas des transactions (pour cela, utiliser `r.pipeline(transaction=True)` qui enveloppe dans `MULTI/EXEC`).
```

## Résumé

Redis est un outil de **niche mais irremplaçable** dans l'architecture moderne des applications : là où SQL excelle pour stocker, relier et interroger des données structurées durables, Redis excelle pour les données **chaudes, temporaires, à faible latence**.

```{prf:remark}
:label: ch16-rem-synthese

Les points clés à retenir :

- Redis maintient tout en RAM — latences sub-milliseconde, throughput millions d'ops/sec.
- Chaque type de données (String, List, Hash, Set, Sorted Set, Stream) a des opérations dédiées adaptées à des patterns spécifiques.
- Le TTL natif simplifie la gestion du cache et des sessions sans cron de nettoyage.
- Pub/Sub est fire-and-forget ; pour la durabilité, utiliser les Streams.
- RDB (snapshots) et AOF (journal) offrent deux niveaux de persistance combinables.
- Redis complète SQL — il ne le remplace pas. La règle d'or : si votre requête SQL est un goulot d'étranglement et que la donnée est reproductible, mettez-la dans Redis.
```
