Cache#
Kdy se čtení vrátí z Redisu místo z databáze, jak se to zneplatní a co udělat,
když potřebujete čerstvá data. Jádro: app/Core/Cache/, konfigurace
config/Shared/cache.neon.
Co se vlastně cachuje#
Ne dotaz, ale HOTOVÁ odpověď
Cachuje se wire payload — to, co z Api presenteru odchází ven (výstup
extractAll() / extractWithReferences()). Ne SQL, ne entity. Zásah tedy
přeskočí celé sestavení dotazu, hydrataci i serializaci, ale zároveň to
znamená, že v cache leží už poskládaná data pro konkrétní volání.
Hranice je Api presenter. Front i administrace čtou přes ni, takže se obě strany trefují do stejné cache.
Tři vypínače nad sebou#
cache:
config:
enabled: false # hlavní vypínač
enabledServices: [] # allowlist služeb (FQCN); prázdné = nic
writeOnly: false # zapisuj, ale nečti (měření zahřátí)
queryTtl: 300 # 5 minut
| Vrstva | Co dělá |
|---|---|
enabled: false |
nic se nečte ani nezapisuje, celá cache je mimo hru |
enabledServices |
postupné zapínání po službách — cachuje jen to, co je v seznamu |
writeOnly: true |
zapisuje, ale čte z databáze — na ověření, co by se ukládalo |
enabled: true bez enabledServices necachuje NIC
Platí obojí zároveň. Je to schválně: zapnout cache plošně jedním přepínačem znamená zapnout ji i tam, kde se ještě neověřilo, že se invalidace opravdu provede.
Wildcard * až po ověření všech služeb
enabledServices: ['*'] zapne všechno naráz. Dokud každá služba nemá pokryté
zápisy mimo ORM (raw DBAL), znamená to servírovat stará data — a nikdo si
toho nevšimne, protože stránka se tváří normálně.
Jak se cache zneplatní#
Klíč nese tagy = FQCN entit, ze kterých se odpověď skládá. Tagy se odvozují automaticky z toho, co dotaz použil:
kořenová entita + includes (relace) + pole ve filtrech + pole v řazení
└────────────── DependencyTagResolver ──────────────┘
Zápis pak jen zvedne verzi tagu (bumpFor() / bumpForClass()). Staré klíče
tím rázem přestanou platit — nic se nemaže, jen se přestanou trefovat.
Proto se nemusí vypisovat, co se má smazat
Uložení článku zvedne verzi tagu Article a všechny odpovědi, které článek
obsahovaly — výpisy, detaily, related boxy — jsou naráz mimo. Bez seznamu
klíčů, který by stejně nikdo neudržel aktuální.
Zápis MIMO ORM tagem nehne
Raw DBAL UPDATE, updateColumn(), databázový trigger — nic z toho nespustí
hook, takže se verze tagu nezvedne a cache dál vrací stará data až do vypršení
TTL. Kdo píše raw SQL, musí bump zavolat sám.
Postup: potřebuji čerstvá data pro jedno volání#
Nejčastější případ. Nesahá se na konfiguraci, jen na to jedno volání:
$payload = $this->bridge->call('api/blog/article/get-all', [
'filters' => $filters,
ApiBridge::PARAM_USE_CACHE => false, // ← tohle volání jde vždy do DB
], null, 'POST');
| Hodnota | Význam |
|---|---|
false |
nečíst z cache ani do ní nezapisovat |
true |
cachovat, i kdyby to volající jinak nedělal |
null / vynechat |
„bez názoru“ — rozhodne konfigurace |
Parametr se z volání odstraní, do Api metody nedoteče
Řídí bridge, ne endpoint. Api metoda ho ve své signatuře nemá a nesmí mít —
jinak skončí Unknown named parameter.
Postup: přidávám čtení, které se má cachovat#
- Ověřit, že všechny zápisy jdou přes manager — tedy že se bump opravdu spustí.
- Dohledat zápisy mimo ORM v témž modulu (raw DBAL,
updateColumn, triggery) a doplnit u nichbumpForClass(). - Zapnout
writeOnly: truea projít stránku — do Redisu se začne zapisovat, čte se pořád z databáze. - Porovnat, co se uložilo, s tím, co má stránka zobrazit.
- Přidat službu do
enabledServices— teprve teď se z cache čte. - Prokliknout scénář zápis → čtení: uložit, hned načíst a ověřit, že je vidět nová hodnota.
Krok 6 nejde přeskočit
Chybějící invalidace se neprojeví chybou, ale starým údajem — a ten vypadá jako pravda. Nejlevnější doba, kdy se to dá odhalit, je hned teď.
Co se necachuje schválně#
| Kde | Proč |
|---|---|
| Administrace | politika pro celý požadavek se ve startup() vypne — administrátor musí vidět, co právě uložil |
| Hledání přes Elasticsearch | výsledek se mění bez zápisu do databáze (reindex, změna skóre), takže by cache neměla podle čeho invalidovat |
Dotazy s NonCacheableFilter |
klíč by musel obsahovat celý seznam id |
Administrace vypíná cache i pro frontové managery
Když v admin požadavku běží čtení přes front manager, platí i na něj. Jinak by administrátor viděl v jednom místě čerstvá data a ve druhém stará.
Další cache v systému#
| Cache | Kde | Poznámka |
|---|---|---|
| Route tabulka | app/Core/Cache/Route/ |
vlastní vypínač routerEnabled, nezávislý na hlavním |
| Session | Redis | jiný mechanismus, jiná životnost |
| Doctrine, DI kontejner, Latte | temp/cache/ |
soubory, ne Redis |
Katalog překladů se sám neobnoví
Nový nebo změněný jazykový .neon se neprojeví, dokud se nesmaže
temp/cache/translation/. Nette si jinak zregeneruje skoro všechno samo —
tohle ne, a projeví se to vypsáním syrových klíčů místo textů.
Když se zdá, že cache lže#
Postup od nejlevnějšího
- Zopakovat volání s
PARAM_USE_CACHE => false. Když je výsledek správně, je to invalidace, ne dotaz. - Najít, kudy se ta hodnota zapisuje — jde to přes manager, nebo raw SQL?
- Zkontrolovat, že tag odpovídá entitě, na které se změna stala; odvozuje se z includes a filtrů, takže nečekaná relace může chybět.
- Teprve pak sahat na TTL. TTL je pojistka proti dírám, ne řešení.
Výpadek Redisu cache tiše vypne
První chyba spojení ji pro celý požadavek odstaví a jede se z databáze. Je to záměr — web má fungovat i bez Redisu — ale znamená to, že „cache se neprojevuje“ může znamenat „Redis neběží“.