Redis jako cache: cache-aside, TTL, invalidace a časté chyby
Jak zrychlit aplikaci pomocí Redisu: instalace, vzor cache-aside v Pythonu a Node.js, nastavení TTL a maxmemory, invalidace a ochrana před cache stampede.
Když databáze opakovaně počítá stejnou věc – přehled produktů, počty, výsledek drahého dotazu – je čas na cache. Redis je in-memory úložiště klíč–hodnota s odezvou v řádu stovek mikrosekund a pro tuto roli je nejčastější volbou. Ukážeme, jak ho nasadit správně a neudělat si z cache zdroj chyb.
Instalace a základní nastavení
Na Ubuntu/Debianu:
sudo apt install -y redis-server
redis-cli ping # odpověď: PONG
Konfigurace je v /etc/redis/redis.conf. Pro cache jsou důležité tři věci:
bind 127.0.0.1 -::1
maxmemory 256mb
maxmemory-policy allkeys-lru
bind– Redis má naslouchat jen lokálně. Redis vystavený do internetu bez hesla je jeden z nejčastějších způsobů, jak přijít o server.maxmemory– horní limit paměti.maxmemory-policy allkeys-lru– při zaplnění Redis zahodí nejdéle nepoužité klíče. Výchozínoevictionby místo toho začal odmítat zápisy.
sudo systemctl restart redis-server
Pokud k Redisu přistupuje více aplikací nebo jiný server, nastavte heslo (requirepass) nebo uživatele přes ACL.
Poznámka k licenci: Redis od verze 7.4 změnil licenci a od 8.0 přidal i AGPL. Některé distribuce proto nabízejí kompatibilní fork Valkey – příkazy i klientské knihovny fungují stejně.
Vzor cache-aside
Nejběžnější a nejbezpečnější vzor:
- Aplikace se podívá do cache.
- Pokud data najde (hit), vrátí je.
- Pokud ne (miss), načte je z databáze, uloží do cache s expirací a vrátí.
Python (redis-py)
import json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def get_product(product_id: int) -> dict:
key = f"product:{product_id}:v1"
cached = r.get(key)
if cached is not None:
return json.loads(cached)
product = db_fetch_product(product_id) # váš dotaz do databáze
r.set(key, json.dumps(product), ex=600) # TTL 10 minut
return product
Node.js (node-redis v4+)
import { createClient } from "redis";
const redis = createClient({ url: "redis://localhost:6379" });
await redis.connect();
export async function getProduct(id) {
const key = `product:${id}:v1`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await dbFetchProduct(id);
await redis.set(key, JSON.stringify(product), { EX: 600 });
return product;
}
Vždy nastavte TTL
Klíč bez expirace v cache žije, dokud ho Redis nevyhodí kvůli paměti – a mezitím může servírovat zastaralá data. TTL je pojistka: i když zapomenete cache invalidovat, data se nejpozději po uplynutí TTL obnoví.
Kontrola v redis-cli:
redis-cli TTL product:42:v1 # zbývající sekundy, -1 = bez expirace, -2 = neexistuje
Invalidace
Při změně dat v databázi klíč smažte:
def update_product(product_id: int, data: dict) -> None:
db_update_product(product_id, data)
r.delete(f"product:{product_id}:v1")
Klíč mažte, nepřepisujte. Při souběžných požadavcích by přepis mohl uložit starší verzi dat přes novější. Smazání znamená, že další čtení si načte aktuální stav.
Přípona :v1 v názvu klíče se hodí při změně formátu dat: zvýšíte verzi na v2 a staré klíče postupně vyprší.
Nepoužívejte KEYS v produkci
KEYS product:* projde celou databázi a zablokuje Redis (je jednovláknový pro příkazy). Pro procházení klíčů použijte SCAN:
redis-cli --scan --pattern 'product:*' | head
Cache stampede
Populární klíč vyprší a v tu chvíli sto souběžných požadavků zjistí miss a všechny se zeptají databáze. Obrana:
- Zámek – jen první požadavek načítá data, ostatní chvíli počkají nebo dostanou starou hodnotu:
if r.set(f"lock:{key}", "1", nx=True, ex=10):
try:
value = load_from_db()
r.set(key, json.dumps(value), ex=600)
finally:
r.delete(f"lock:{key}")
- Náhodný rozptyl TTL (
ex=600 + random.randint(0, 60)), aby klíče nevypršely všechny najednou.
Co do cache patří a co ne
Ano: výsledky drahých dotazů, data z pomalých externích API, vyrenderované fragmenty stránek, session, počítadla pro rate limiting.
Ne: data, která musí být vždy přesná (zůstatek na účtu, sklad při objednávce), a nic, co nemáte jinde uloženo – cache může kdykoli zmizet.
Monitoring
redis-cli INFO stats | grep -E 'keyspace_(hits|misses)|evicted_keys'
redis-cli INFO memory | grep used_memory_human
redis-cli --latency
Poměr keyspace_hits ku keyspace_misses ukazuje, jestli cache pomáhá. Rostoucí evicted_keys znamená, že je maxmemory malé.
Shrnutí
- Redis jen na localhost nebo za heslem.
maxmemory+allkeys-lru.- Cache-aside, TTL u každého klíče, při změně dat klíč smazat.
SCANmístoKEYS.- Aplikace musí fungovat i při výpadku Redisu – chybu cache zalogujte a čtěte z databáze.