Przejdź do treści
Wydajność 6 min czytania

Strategie cache w sklepie B2B - TTL, invalidacja, hit ratio

Cache to nie „włącz Redis i działa". Cache to seria świadomych decyzji: co cache'ować, na ile długo, kiedy invalidować, jak monitorować. Złe decyzje skutkują albo bardzo niskim hit ratio (cache miss → wolny sklep), albo nieświeżymi danymi (klient widzi wczorajszą cenę). Ten artykuł to konkretne polityki, których używam.

Jakub Owsianka Autor
Zaktualizowano:
Okladka artykulu: Strategie cache w sklepie B2B - TTL, invalidacja, hit ratio (kategoria: Wydajność)
Okladka artykulu: Strategie cache w sklepie B2B - TTL, invalidacja, hit ratio (kategoria: Wydajność)
Spis treści (9)

W skrócie

  • 1. TTL = funkcja zmienności danych. Cenniki kontraktowe 5-15 min, stany 30-60s, statyki rok.
  • 2. Invalidacja eventowa (webhook → flush) > tylko TTL.
  • 3. Hit ratio targety: APCu >95%, Redis >80%, Varnish >70%, CDN >85%.
  • 4. Monitoring obowiązkowy. Cache bez metryk = ślepa wiara.
  • 5. Stale-while-revalidate dla najlepszego UX (serwuj stary cache + odśwież w tle).

TTL per typ danych

Najważniejsza decyzja architektoniczna. Tabela TTL, której używam dla typowego sklepu B2B:

Typ danych Warstwa TTL Strategia invalidacji
Konfiguracja aplikacji APCu 300s Flush przy deploy
Kategorie (drzewo) Redis 1h Webhook z PIM
Atrybuty produktów Redis 30 min Webhook z PIM / Magento
Ceny katalogowe Redis 30 min Webhook z ERP
Ceny kontraktowe Redis 5-15 min Webhook z ERP + cron
Stany magazynowe Redis 30-60s Webhook z WMS/ERP + pull on-demand
Limit kredytowy klienta Redis 1-5 min Walidacja synchronizna przy checkout
Wyniki search (top queries) Redis 5-30 min TTL + reindex Elastic
HTML strony niezalogowani Varnish 1h Tag-based + manual flush
HTML fragmenty (ESI) Varnish 5-30 min Tag-based
Obrazy produktów CDN 30 dni Hash w URL (immutable)
CSS / JS bundles CDN 1 rok Hash w URL (immutable)
Fonty webfonts CDN 1 rok Wersja w URL

Najtrudniejsze są ceny kontraktowe i stany - te wymagają najwięcej uwagi.

Cenniki kontraktowe - case study

Sklep 30k SKU, 200 klientów B2B, każdy ma własny cennik. Bez cache: każda strona produktu wywołuje ERP, sklep umiera w peak'u.

Strategia:

1. Klucz cache: price:{customer_id}:{sku} lub price:{customer_id}:{sku}:{qty} (jeśli progi ilościowe).

2. TTL: 10 min. Kompromis między „świeże dane" a „nie zabij ERP".

3. Cache aside pattern:

GET price for customer K001, SKU ABC, qty 5
  -> check Redis: "price:K001:ABC:5"
    -> HIT: return cached
    -> MISS: call ERP, store in Redis with TTL 600s, return

4. Invalidacja:

  • TTL wygasa
  • Webhook z ERP przy zmianie cennika klienta → FLUSH price:K001:*
  • Manual flush w panelu (przycisk „Wyczyść cache cen dla klienta")

5. Batch fetching: PLP z 50 produktami → batch ERP call, nie 50 osobnych.

6. Cache warming: Po deploy / po flush cache, cron-job rozgrzewa cache top 1000 kombinacji (klient, produkt) w nocy.

Wynik: hit ratio >85%, średnia latency 50ms zamiast 300ms (ERP).

Stany magazynowe - case study

Inny problem. Stany zmieniają się częściej (zamówienia, dostawy), wymagają świeżości.

Strategia hybrydowa:

1. Background sync ERP → sklep. Co 1-5 minut delta synchronizacja (zmienione od ostatniego pinga).

2. Cache w sklepie + Redis. Sklep ma własny widok stanów. Cache w Redis TTL 60s.

3. On-demand pull dla konkretnego SKU. Klient otwiera PDP → sklep robi quick check do ERP (TTL 30s w Redis).

4. Walidacja synchronizna przy checkout. Klient klika „Zamów" → sklep pyta ERP o aktualny stan, blokuje rezerwację.

5. Stan zarezerwowany ≠ stan magazynowy. Stan dostępny = stan magazynowy - rezerwacje w toku. Te dwie wartości żyją osobno.

Wynik: klient widzi stan świeży w okolicach 30-60s. Brak overselling.

HTML cache dla niezalogowanych - Varnish

Strony publiczne (homepage, blog, kategorie publiczne, PDP dla niezalogowanych) świetnie się cache'ują.

Konfiguracja Varnisha dla sklepu:

sub vcl_recv {
    # Standardowo nie cachuj jeśli zalogowany
    if (req.http.Cookie ~ "PHPSESSID|frontend_session") {
        return (pass);
    }

    # Wybór: cachuj koszyk dla anonimowych
    if (req.url ~ "^/checkout/cart") {
        return (pass);
    }

    # Wszystko inne cachowalne
    return (hash);
}

sub vcl_backend_response {
    # Domyślny TTL 1h
    set beresp.ttl = 1h;

    # Statyki dłużej
    if (bereq.url ~ "\.(css|js|jpg|webp|woff2)$") {
        set beresp.ttl = 30d;
    }

    # Strony produktów krócej
    if (bereq.url ~ "^/produkt/") {
        set beresp.ttl = 15m;
    }
}

Hit ratio target: 70%+ dla niezalogowanych. Realne wartości w polskim B2B: 60-85% w zależności od ratio anonim/zalogowani.

ESI - fragmenty dynamic w cache'owanym HTML

Klient B2B zalogowany. Strona produktu w 90% taka sama jak dla wszystkich, ale cena i stan zależne od klienta.

Edge Side Includes (ESI) pozwala mieszać:

<!-- Cachowana strona produktu (1h Varnish cache) -->
<div class="product">
  <h1>Nazwa produktu</h1>
  <img src="...">
  <p>{opis produktu}</p>

  <!-- Te fragmenty pobierane per-user, NIE cachowane razem z HTML -->
  <esi:include src="/fragment/price/{customer_id}/{sku}"/>
  <esi:include src="/fragment/stock/{sku}"/>
  <esi:include src="/fragment/credit-info/{customer_id}"/>
</div>

Varnish cachuje główną stronę. ESI fragments cachowane osobno (Redis, 5-10 min TTL). Klient widzi mix.

Wymaga: wsparcia ESI po stronie aplikacji (Symfony ma natywne, Magento - moduły).

Stale-while-revalidate

Pattern z CDN-ów, teraz wspierany przez przeglądarki i fetch API.

Idea: klient otrzymuje stary cache (szybko), aplikacja w tle odświeża cache. Następne żądania dostają już nowy.

Cache-Control: max-age=300, stale-while-revalidate=600

Browser/CDN:

  • Pierwsze 300s: serwuj świeży cache
  • Między 300-900s: serwuj stary cache + odświeżaj w tle
  • Po 900s: blocking fetch nowego cache

Wartość: UX idealny - klient nigdy nie czeka na regenerację. Latencja od strony klienta praktycznie zero.

Wsparcie: Cloudflare, Fastly, Varnish. W aplikacji - Symfony HttpCache, ręcznie.

Tag-based invalidation

Klasyczne TTL - wystarczające. Tag-based - eleganckie.

Idea: każdy cache item ma tagi. Flush per tag invaliduje wszystko z tym tagiem.

// Magento przykład (PSR-6 cache)
$cache->save('product_123_html', $html, ['catalog_product_123', 'catalog_category_45']);

// Później, gdy produkt 123 się zmieni:
$cache->invalidateTags(['catalog_product_123']);
// Flushuje wszystkie cache items z tym tagiem

W Symfony:

$item = $cacheItemPool->getItem('product_123');
$item->set($data)->tag(['product_123', 'category_45']);
$cacheItemPool->save($item);

// Invalidacja
$cacheItemPool->invalidateTags(['product_123']);

Wartość: zamiast martwić się „które klucze flushować", flushujesz po tag.

Wsparcie: Redis (PSR-6 z tag adapterem), Memcached (z opakowaniem), Varnish (z ban patterns).

Monitoring hit ratio - must-have

Bez monitoringu cache to bomba zegarowa.

Metryki:

1. Hit ratio per cache layer.

APCu hit ratio: 97% → ok
Redis hit ratio (prices): 87% → ok
Redis hit ratio (stocks): 62% → niski, ale akceptowalny (stany się zmieniają)
Varnish hit ratio: 71% → ok dla niezalogowanych mix
CDN hit ratio: 89% → ok

2. Cache miss latency. Jak długo trwa wygenerowanie strony przy cache miss. Jeśli >3s - naprawiamy.

3. Cache size. Czy nie evict'ujesz cache (LRU usuwa stare). Redis OOM = problem.

4. Hot keys. Najpopularniejsze klucze. Pozwala wykryć anomalie (np. jeden klient ma 1000 wpisów w cache cen - może wadliwie skonfigurowany worker generuje śmieci).

Narzędzia:

  • Redis: INFO, MONITOR (uważnie - drogi), Redis Exporter dla Prometheus
  • Varnish: varnishstat, varnishlog
  • CDN: dashboard providera (Cloudflare Analytics, Fastly Insights)
  • Custom: Prometheus + Grafana, własne metryki w aplikacji

Najczęstsze błędy

1. Brak Redis dla cache cen kontraktowych. Każda strona produktu wywołuje ERP. ERP umiera.

2. Za długie TTL na ceny. TTL 1h → klient widzi cenę z poprzedniej godziny. Po godzinie rozjazdu - reklamacja.

3. Za krótkie TTL na statyki. CSS cache 5 min → klient pobiera 12 razy / godzinę. Bandwidth.

4. Cache HTML zalogowanych jako całość. Klient A widzi cenę klienta B. Katastrofa danych.

5. Brak invalidation event-based. Tylko TTL → w peak'u stale dane.

6. Brak monitoringu hit ratio. Cache „działa", ale w peak'u nie cachuje. Nikt nie wie.

7. Wspólny Redis dla wszystkiego. Sesje + cache produktów + cache cen → OOM przy peak'u. Separate instances.

FAQ

Czy Magento ma wbudowany cache? Tak, na poziomie obiektów (collection cache, block cache), full-page (FPC), Varnish jako alternative. Konfigurowalne.

Czy mogę używać Memcached zamiast Redis? Można. Redis ma więcej funkcji (persistence, structures, pub/sub). Nowe projekty PHP/Symfony - Redis.

Co z cache PHP OPcache? OPcache cache'uje kod PHP (kompilację). Niezależny od application cache. Zawsze włączony w produkcji.

Czy Varnish to konieczność? Dla Magento - praktycznie tak (bez Varnish FPC sklep wolny). Dla Shopware - opcjonalne, ale dla skali B2B zalecane.

Jak długo cache utrzymywać po awarii? Zazwyczaj TTL ratuje (cache się odbuduje). Po dużej awarii - cache warm-up cron, rozgrzanie top kluczy.

Co dalej

O autorze

Jakub Owsianka

Architekt rozwiązania w WiseB2B - silniku platform B2B. Zaczynał po stronie biznesu (własne sklepy), potem deweloper, dziś projektuje wdrożenia dla sklepów z katalogami w dziesiątkach tysięcy SKU. W ostatnich latach wdrożył AI-development w zespole i funkcjonalności oparte o AI bezpośrednio w silniku sklepu.

Masz pytanie do tego artykułu?

Dodatkowy kontekst, problem z własnym wdrożeniem, druga opinia - napisz wprost. Odpowiadam osobiście w 1-2 dni robocze.