Una query che impiega 200ms sul tuo database impiega 200ms anche alla milionesima esecuzione, e ogni esecuzione compete per le stesse connessioni, lo stesso I/O su disco e la stessa CPU. Redis tiene la risposta in memoria, su un server che non fa altro, così la milionesima lettura costa un round trip di rete invece di un piano di query.

Cosa fa davvero Redis come cache

Redis è un key-value store in memoria. I valori vivono in RAM, quindi letture e scritture si misurano in tempi sotto il millisecondo invece delle decine o centinaia di millisecondi che costa una query relazionale una volta che entrano in gioco join, indici sotto pressione o un connection pool occupato. Si affianca al tuo database, non lo sostituisce: la tua applicazione interroga prima Redis e va al database solo quando Redis non ha la risposta.

Il pattern per questo si chiama cache-aside (anche “lazy loading”), ed è il primo a cui la maggior parte delle applicazioni ricorre:

  1. L’applicazione chiede una chiave a Redis.
  2. Cache hit — Redis ce l’ha, la restituisce. Il database non vede nemmeno la richiesta.
  3. Cache miss — Redis non ce l’ha. Interroga il database, scrive il risultato in Redis, poi lo restituisce.
1
2
3
4
5
Request → Redis? ──hit──→ restituisce il valore
            miss
          Database → scrive in Redis → restituisce il valore

Il database resta la fonte di verità. Redis tiene una copia veloce e usa e getta delle parti che leggi spesso. Se Redis riparte vuoto, non perdi nulla: si ripopola da solo al giro successivo di cache miss.

Come avviare Redis con Docker

Il modo più rapido per avere un’istanza Redis è l’immagine ufficiale. Se non hai mai usato Docker, Cos’è Docker e a cosa serve: guida per chi inizia copre le basi prima di proseguire.

1
docker run --name redis-cache -d -p 6379:6379 redis:8

Questo avvia Redis 8 in background, pubblicando la porta 6379 sulla tua macchina. Connettiti con redis-cli, installato localmente oppure lanciato da un secondo container sulla stessa rete:

1
docker run -it --rm --network container:redis-cache redis:8 redis-cli
1
2
3
4
5
6
127.0.0.1:6379> SET user:42:name "Elena"
OK
127.0.0.1:6379> GET user:42:name
"Elena"
127.0.0.1:6379> TTL user:42:name
(integer) -1

TTL che restituisce -1 significa che la chiave non ha una scadenza: resta finché non la cancelli o Redis non la elimina per pressione sulla memoria. Per una cache vuoi quasi sempre una scadenza, così i dati vecchi decadono da soli.

1
2
3
4
127.0.0.1:6379> SET user:42:name "Elena" EX 300
OK
127.0.0.1:6379> TTL user:42:name
(integer) 297

EX 300 imposta la chiave a scadere tra 300 secondi. PX fa lo stesso in millisecondi, e EXPIRE user:42:name 300 imposta un TTL su una chiave che esiste già.

Per qualsiasi cosa oltre un controllo manuale veloce, esegui Redis tramite Compose insieme al resto del tuo stack — vedi Cos’è Docker Compose e Come si Usa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  redis:
    image: redis:8
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --save 60 1 --maxmemory 256mb --maxmemory-policy allkeys-lru

  api:
    build: .
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379

volumes:
  redis-data:

api raggiunge Redis all’hostname redis perché Compose mette entrambi i servizi sulla stessa rete definita dall’utente, il meccanismo descritto in Cos’è una Rete Docker e Come si Usa. Il volume redis-data conta meno per una cache pura che per un database primario (vedi Cos’è un volume Docker e come si usa): perdere la cache significa solo che le letture successive diventano cache miss. Mantienilo comunque se avere una cache già calda dopo un riavvio fa la differenza per la tua latenza.

Come scrivere la logica cache-aside nella tua applicazione

Redis ti dà i comandi; la tua applicazione deve comunque chiamarli nell’ordine giusto. Ecco come si avvolge una lettura dal database nel pattern cache-aside:

1
2
3
4
5
6
7
8
9
def get_user(user_id):
    key = f"user:{user_id}"
    cached = redis.get(key)
    if cached is not None:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.set(key, json.dumps(user), ex=300)
    return user

Due dettagli contano più di tutto il resto:

  • Il TTL (ex=300) non è opzionale. Senza, una riga che cambia nel database continua a servire il suo vecchio valore da Redis per sempre. Imposta il TTL in base a quanto è accettabile un dato non aggiornato per quella specifica chiave, non un numero unico per tutta l’applicazione.
  • json.dumps/json.loads. Redis memorizza stringhe, più una manciata di tipi più ricchi come hash e sorted set — mai gli oggetti del tuo linguaggio. Qualsiasi cosa strutturata va serializzata in entrata e riletta in uscita.

Quando la riga sottostante cambia, cancella la chiave invece di aspettare il TTL:

1
2
3
def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")

La prossima lettura dopo un aggiornamento è un cache miss garantito, che ripopola Redis con la riga fresca. Cache-aside non tenta mai di tenere la cache sincronizzata in tempo reale. Fa sparire velocemente le voci non aggiornate, per TTL o per cancellazione esplicita alla scrittura.

Come funziona l’eviction quando Redis esaurisce la memoria

Redis tiene tutto in RAM, quindi la memoria ha un limite rigido che il disco, di norma, non ha. Imposta un tetto con maxmemory, e dì a Redis cosa fare quando lo raggiunge con maxmemory-policy:

PoliticaComportamentoAdatta per
noevictionRifiuta nuove scritture una volta piena, restituisce un erroreUn datastore che non può perdere dati silenziosamente
allkeys-lruElimina la chiave usata meno di recente, qualsiasi chiaveUna cache pura — la scelta di default sensata
volatile-lruElimina la chiave usata meno di recente tra le chiavi con un TTLMischiare chiavi di cache e chiavi permanenti in un’istanza
volatile-ttlElimina prima la chiave con il TTL residuo più cortoRate limiter, token a breve vita

Per un’istanza di cache dedicata, allkeys-lru è quasi sempre corretta: ogni chiave al suo interno è usa e getta per definizione, quindi lascia che Redis elimini quello che hai toccato meno di recente. noeviction è la politica sbagliata per una cache, perché trasforma “Redis è pieno” in errori applicativi invece di far sparire silenziosamente le voci fredde.

1
2
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru

Imposta entrambi nel comando di avvio, come nel file Compose sopra, non solo a runtime. Un riavvio del container azzera un CONFIG SET impostato solo a runtime, riportando Redis ai suoi default (maxmemory 0, cioè illimitato, limitato solo dall’host).

Quando la cache Redis è la scelta sbagliata

Il caching nasconde un percorso lento; non lo ripara. Quattro situazioni in cui ricorrere a Redis per primo peggiora le cose:

  • I dati cambiano a ogni lettura comunque. Un ticker azionario in tempo reale o un contatore incrementato a ogni richiesta non guadagna nulla da una cache invalidata tanto velocemente quanto viene popolata. Hai aggiunto un hop di rete senza hit rate.
  • Hai bisogno di consistenza forte. Cache-aside è eventually consistent per design: tra una scrittura e il cache miss successivo esiste una finestra in cui può essere servito ancora un valore non aggiornato. Per un saldo di conto o una quantità di magazzino dove quella finestra causa danni reali, non metterla in cache, oppure mettila in cache con un TTL di pochi secondi e accetta il compromesso esplicitamente.
  • Il vero collo di bottiglia è altrove. Se il tuo database è lento per un indice mancante o una query N+1, risolvi prima quello. Mettere in cache una query scritta male sposta la stessa risposta sbagliata in memoria più velocemente.
  • Un thundering herd alla scadenza. Quando una chiave calda scade, tutte le richieste concorrenti possono fallire la cache insieme e martellare il database simultaneamente. Usa SET key value NX EX 30 come lock a breve durata, tenuto da qualunque richiesta ripopoli la cache, così le altre aspettano quella invece di accumularsi sul database.

Da dove iniziare con la cache Redis

Metti un container redis:8 davanti alla tua lettura più lenta e più ripetuta. Avvolgila in cache-aside con un TTL che sai giustificare, non un numero tondo scelto a caso, e imposta maxmemory-policy allkeys-lru così una cache piena degrada invece di generare errori. Poi misura l’hit rate. Write-through caching, invalidazione via pub/sub e Redis Streams costano tutti complessità, e nessuno vale la pena finché un semplice setup cache-aside non ti ha mostrato esattamente dove si ferma.

Articoli correlati