Piccolo sistema di cache con Redis, ovvero come evitare di chiedere sempre le stesse cose al povero database

Prima o poi succede.

Hai un’applicazione, magari fatta bene, magari fatta di corsa, magari entrambe le cose perché la vita è questa, e ti accorgi che alcune ricerche vengono fatte sempre uguali.

L’utente cerca “Roma”.

Poi un altro cerca “Roma”.

Poi un altro ancora cerca “Roma”.

A un certo punto il database inizia a guardarti come un cameriere a cui hai chiesto il conto sette volte.

La soluzione non è sempre ottimizzare query, aggiungere indici, riscrivere mezzo backend o invocare antichi spiriti DBA.

A volte basta una cache.

E Redis, in questo caso, è uno di quegli strumenti semplici, veloci e molto pratici. Gli dici: "Tienimi da parte questa risposta per un po’". Lui lo fa e senza discutere. Che già lo rende più collaborativo di molte riunioni.

L’idea

Creiamo una piccola architettura con Docker Compose. 

Avremo tre container.

A. Uno per l’applicazione web, 

B. Uno per Redis 

C. Uno per un database. 

In questo esempio il database lo lasciamo finto, perché il punto è capire il meccanismo.

Il flusso è semplice: quando arriva una ricerca, l’applicazione controlla prima Redis.

Se Redis ha già il risultato, lo restituisce subito.

Se Redis non lo ha, l’applicazione fa la ricerca “vera”, salva il risultato in Redis e poi lo restituisce.

La volta dopo sarà tutto più veloce.

È un po’ come ricordarsi dove hai messo il cacciavite invece di ricominciare ogni volta a svuotare il garage.

docker-compose.yml

Partiamo dal file docker-compose.yml.

services:
  app:
    build: .
    container_name: search_app
    ports:
      - "8000:8000"
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      - redis

  redis:
    image: redis:7
    container_name: search_redis
    ports:
      - "6379:6379"

Qui succede una cosa importante. L’applicazione non si collega a Redis usando localhost. Dentro Docker Compose, i container si parlano usando il nome del servizio, quindi l’app chiama Redis semplicemente con hostname redis.

Questa cosa sembra banale, ma ha salvato più sviluppatori di quanto siamo disposti ad ammettere.

Una piccola app FastAPI

Ora immaginiamo una piccola API Python.

Creiamo main.py.

import json
import os
import time

import redis
from fastapi import FastAPI

app = FastAPI()

redis_client = redis.Redis(
    host=os.getenv("REDIS_HOST", "localhost"),
    port=int(os.getenv("REDIS_PORT", 6379)),
    decode_responses=True,
)


def fake_expensive_search(query: str) -> dict:
    """
    Simula una ricerca lenta.
    Nella vita vera qui ci sarebbe una query su database,
    una chiamata API o qualche altra cosa che costa tempo.
    """
    time.sleep(2)

    return {
        "query": query,
        "results": [
            f"Risultato 1 per {query}",
            f"Risultato 2 per {query}",
            f"Risultato 3 per {query}",
        ],
        "source": "database"
    }


@app.get("/search")
def search(q: str):
    cache_key = f"search:{q.lower().strip()}"

    cached_result = redis_client.get(cache_key)

    if cached_result:
        data = json.loads(cached_result)
        data["source"] = "redis"
        return data

    result = fake_expensive_search(q)

    redis_client.setex(
        cache_key,
        300,
        json.dumps(result)
    )

    return result

Il cuore è qui.

cached_result = redis_client.get(cache_key)

Prima chiediamo a Redis se conosce già la risposta.

Se la conosce, perfetto. Se non la conosce, facciamo la ricerca lenta e poi salviamo il risultato per 300 secondi, cioè 5 minuti.

redis_client.setex(cache_key, 300, json.dumps(result))

setex significa: salva questa chiave, ma non per sempre.

Perché la cache eterna è bellissima fino al giorno in cui mostra dati vecchi e tutti iniziano a guardare te.

Dockerfile

Aggiungiamo un Dockerfile semplice.

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

CMD ["fastapi", "run", "main.py", "--host", "0.0.0.0", "--port", "8000"]

E il file requirements.txt.

fastapi[standard]
redis

Avvio

Ora possiamo avviare tutto.

docker compose up -d --build

Poi proviamo la ricerca.

curl "http://localhost:8000/search?q=Roma"

La prima volta ci mette circa due secondi.

Il risultato arriva dal “database” finto.

{
  "query": "Roma",
  "results": [
    "Risultato 1 per Roma",
    "Risultato 2 per Roma",
    "Risultato 3 per Roma"
  ],
  "source": "database"
}

Se rifacciamo subito la stessa chiamata:

curl "http://localhost:8000/search?q=Roma"

questa volta risponde immediatamente.

E la sorgente sarà Redis.

{
  "query": "Roma",
  "results": [
    "Risultato 1 per Roma",
    "Risultato 2 per Roma",
    "Risultato 3 per Roma"
  ],
  "source": "redis"
}

Ed è qui che di solito si prova quel piccolo piacere tecnico che non cambierà il mondo, ma per cinque minuti ti fa sentire una persona ordinata.

Una nota sulle chiavi

La chiave che abbiamo usato è questa.

cache_key = f"search:{q.lower().strip()}"

Così Roma, roma e roma finiscono nella stessa cache.

In un progetto vero potremmo rendere la chiave più robusta, magari includendo lingua, filtri, ordinamento, pagina e utente.

Per esempio:

cache_key = f"search:{lang}:{page}:{query}"

Perché se salvi in cache una ricerca senza considerare i filtri, prima o poi qualcuno cercherà hotel a Roma con piscina e gli restituirai campeggi in Molise e non è bello.

Quando invalidare la cache

La domanda seria è: quanto deve durare una cache? Beh.... dipende. Che è la risposta più odiata dell’informatica, ma purtroppo spesso è quella giusta. Se i dati cambiano raramente, puoi tenere la cache per minuti o ore, ma se cambiano spesso, meglio pochi secondi. Se sono dati critici, magari la cache non ci va proprio.

Nel nostro esempio abbiamo usato 300 secondi.

redis_client.setex(cache_key, 300, json.dumps(result))

È un valore tranquillo per capire il meccanismo. Non troppo lungo da creare disastri, ma non troppo corto da rendere Redis un soprammobile.

Conclusione

Redis non risolve tutti i problemi, non rende bella una query brutta e non trasforma un’architettura confusa in una cattedrale. Insomma non sostituisce il buon senso. Però è uno strumento fantastico per evitare di rifare sempre lo stesso lavoro e in fondo buona parte dell’informatica è proprio questo. Fare una cosa una volta sola e poi convincere le macchine a ricordarsela.

Commenti

Post più popolari