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:
- Jitter TTL per distribuire le scadenze
- Locking per evitare rigenerazioni multiple
- 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.