Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Jádro / Cache

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#

  1. Ověřit, že všechny zápisy jdou přes manager — tedy že se bump opravdu spustí.
  2. Dohledat zápisy mimo ORM v témž modulu (raw DBAL, updateColumn, triggery) a doplnit u nich bumpForClass().
  3. Zapnout writeOnly: true a projít stránku — do Redisu se začne zapisovat, čte se pořád z databáze.
  4. Porovnat, co se uložilo, s tím, co má stránka zobrazit.
  5. Přidat službu do enabledServices — teprve teď se z cache čte.
  6. 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

  1. Zopakovat volání s PARAM_USE_CACHE => false. Když je výsledek správně, je to invalidace, ne dotaz.
  2. Najít, kudy se ta hodnota zapisuje — jde to přes manager, nebo raw SQL?
  3. 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.
  4. 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ěží“.