Cache Stampede su Redis: Come Prevenirlo con Locking e Jitter TTL

Scopri come un improvviso picco di richieste per una chiave cache scaduta può sovraccaricare il tuo database e come fermarlo utilizzando mutex lock, ricomputazione probabilistica anticipata e jitter TTL in Redis.

Cache Stampede su Redis: Come Prevenirlo con Locking e Jitter TTL

Introduzione

Il problema del cache stampede (o thundering herd) si verifica quando una chiave della cache scade e un gran numero di richieste simultanee tenta di ricaricare il dato dal database, causando un sovraccarico improvviso. In questo articolo esploreremo le strategie per mitigare questo fenomeno utilizzando Redis.

Cos'è un Cache Stampede?

Immagina di avere un'applicazione web che memorizza nella cache il risultato di una query costosa. Quando la cache scade, se arrivano 1000 richieste contemporanee, tutte cercheranno di rigenerare il dato, colpendo il database con 1000 query identiche. Questo può portare a:

  • Degrado delle performance
  • Timeout delle richieste
  • Crash del database

Strategie di Prevenzione

1. Mutex Lock (Locking)

Il locking impedisce che più richieste rigenerino contemporaneamente la stessa chiave. Solo la prima richiesta ottiene il lock e rigenera il dato; le altre attendono o usano un valore obsoleto.

import redis
import time

r = redis.Redis()

def get_data_with_lock(key, ttl=60):
    # Prova a ottenere il lock
    lock_key = f"lock:{key}"
    lock_acquired = r.setnx(lock_key, "locked")
    
    if lock_acquired:
        # Imposta TTL sul lock per evit deadlock
        r.expire(lock_key, 10)
        
        # Rigenera il dato (es. dal database)
        data = expensive_query()
        r.setex(key, ttl, data)
        
        # Rilascia il lock
        r.delete(lock_key)
        return data
    else:
        # Attendi e riprova o usa cache obsoleta
        time.sleep(0.1)
        return get_data_with_lock(key, ttl)

2. Ricomputazione Probabilistica Anticipata (Probabilistic Early Recomputation)

Invece di attendere la scadenza esatta, si rigenera la cache in modo probabilistico prima che scada. Questo riduce la probabilità di uno stampede.

import random

def should_recompute(ttl_remaining, beta=1.0):
    """
    Restituisce True se è il momento di ricomputare probabilisticamente.
    beta controlla l'aggressività: più alto = ricomputazione più anticipata.
    """
    if ttl_remaining <= 0:
        return True
    probability = random.random()
    # Formula: probabilità inversamente proporzionale al TTL rimanente
    threshold = 1.0 - (ttl_remaining / (ttl_remaining + beta))
    return probability < threshold

3. Jitter TTL

Aggiungere un piccolo offset casuale al TTL di ogni chiave evita che tutte scadano contemporaneamente.

import random

def set_with_jitter(key, data, base_ttl=60, jitter_range=10):
    jitter = random.randint(0, jitter_range)
    ttl = base_ttl + jitter
    r.setex(key, ttl, data)

Combinare le Strategie

Per una protezione ottimale, combina più tecniche:

  1. Jitter TTL per distribuire le scadenze
  2. Locking per evitare rigenerazioni multiple
  3. Ricomputazione anticipata per ridurre la probabilità di stampede

Esempio Completo

class CacheManager:
    def __init__(self, redis_client):
        self.r = redis_client
    
    def get_or_compute(self, key, compute_func, ttl=60, beta=1.0):
        # Controlla cache
        cached = self.r.get(key)
        if cached is not None:
            ttl_remaining = self.r.ttl(key)
            if not should_recompute(ttl_remaining, beta):
                return cached
        
        # Lock per evitare stampede
        lock_key = f"lock:{key}"
        if self.r.setnx(lock_key, "1"):
            self.r.expire(lock_key, 10)
            try:
                data = compute_func()
                jitter = random.randint(0, 10)
                self.r.setex(key, ttl + jitter, data)
                return data
            finally:
                self.r.delete(lock_key)
        else:
            # Attendi e riprova
            time.sleep(0.05)
            return self.get_or_compute(key, compute_func, ttl, beta)

Conclusioni

Il cache stampede è un problema comune nelle architetture ad alta concorrenza. Utilizzando le tecniche descritte - locking, ricomputazione probabilistica e jitter TTL - puoi proteggere il tuo database e garantire performance stabili. Redis offre gli strumenti necessari per implementare queste soluzioni in modo semplice ed efficace.

Riferimenti