Zum Hauptinhalt springen

Redis Cache

Postbox nutzt Redis als primären Cache-Store mit PostgreSQL als Failover. Die Architektur ist auf kleine, heiße, stabile Antworten ausgelegt — Redis ist L1-Cache, PostgreSQL bleibt Source of Truth.

Setup

Stores

// config/cache.php
'failover' => [
'driver' => 'failover',
'stores' => ['redis', 'database'], // Redis primär, DB-Fallback
],

Connections

DB 0: Default (Locks, Scheduler, Reverb, Queue)
DB 1: Cache (allgemeiner App-Cache)

Redis-Config (Production)

# /etc/redis/redis.conf
maxmemory 512mb # Source-of-Truth — `REDIS_MAX_MEMORY_MB` in .env muss matchen
maxmemory-policy allkeys-lru # LRU-Eviction bei RAM-Knappheit (Page-Cache erwartet das)
save "" # Kein RDB (Cache ist flüchtig)
appendonly no # Kein AOF
requirepass <REDIS_PASSWORD>

TTL-Konfiguration

Alle Cache-TTLs sind zentralisiert in config/postbox.php unter dem Block cache_ttl und können via .env überschrieben werden:

'cache_ttl' => [
'explore_categories' => (int) env('CACHE_TTL_EXPLORE_CATEGORIES', 86400), // 24h
'recommended_global_pool' => (int) env('CACHE_TTL_RECOMMENDED_POOL', 21600), // 6h
'redirects_map' => (int) env('CACHE_TTL_REDIRECTS_MAP', 300), // 5min
// ... ~40 weitere Keys
],

Lese-Pattern: Im Code immer Config::integer('postbox.cache_ttl.X') statt (int) config(...) — PHPStan-strict-type-konform.

Cache::remember('explore:categories', Config::integer('postbox.cache_ttl.explore_categories'), fn () => ...);

Cache-Gruppen

CacheGroupService definiert 17 logische Gruppen mit Prefixen für SCAN-basiertes Bulk-Clearing:

GruppePrefix(e)Auto-RebuildTTL-Bereich
exploreexplore:, discover:24h
public_explorerpublic-explorer:1h–24h
recommendationsrecommended_profiles, trending_slider:1h–6h
pipelinepipeline:, cron:heartbeat:, health:, deploy:⚠ Cron5min–48h
youtubeyt_, youtube_5min–24h
instagraminstagram_1h
tagsblocked_tags:, tag_aliases:1h
adminadmin:30s–1h
serverserver:, db_monitoring:, pulse:, error-monitor:30s–24h
mailmail:flood:⚠ Counter-Reset60s–24h
video_trendsvideo-trends:1h–24h
collectorcollector:5min
explore_sectionsexplore_sections:1h
r2_storager2:✓ (nur bei s3/dual-write)60s–7d
rustfs_storagestorage_monitoring:✓ (nur bei rustfs/dual-write)15min
turnstileturnstile:5min
cache_statscache_stats:24h (intern)

Persistente Keys (PostgreSQL)

Drei forever-Keys liegen explizit im database-Store, weil Redis-Restart sie sonst löscht:

Cache::store('database')->forever('tag_consolidation.established_threshold', $value);
Cache::store('database')->forever('tag_consolidation.auto_execute_high_confidence', $value);
Cache::store('database')->forever('tag_consolidation.cooldown_days', $value);

Jeder neu hinzukommende forever-Key MUSS auf database-Store gehen — siehe docs/10-entwicklung/05-troubleshooting.md.

Admin-Seite /admin/cache-management

Bietet:

  • Redis-Status-Dashboard: Memory, Hit-Rate, Fragmentation, Evicted Keys, Total Keys, Uptime
  • Cache-Gruppen mit Key-Anzahl + "Leeren"-Button (queued via ClearCacheGroup Job + Reverb-Rückmeldung)
  • TTL-Übersicht aus config('postbox.cache_ttl') — alle Keys + lesbare TTL-Werte
  • Cache-Warming-Status des letzten cache:warm-Laufs
  • Hit-Rate-Chart der letzten 24h aus cache_monitoring_snapshots (Snapshot alle 15min)
  • Danger Zone Flush-All mit Bestätigungsmodal

Health-Check /up_system

Der Endpoint enthält jetzt einen dedizierten REDIS-Check mit Kennzahlen:

REDIS RUNNING
keys=1115 mem=7.2% hit_rate=94.3% frag=1.1

Status-Codes:

  • RUNNING — alles im grünen Bereich
  • WARNINGevicted_keys > 0 ODER memory > 80% ODER fragmentation > 1.5
  • FAILED — Redis nicht erreichbar

Cache-Warming nach Deploy

# Warmt die wichtigsten Caches proaktiv
php artisan cache:warm

# Optionen
php artisan cache:warm --rate=50 # Custom rate (Keys pro Minute)
php artisan cache:warm --group=explore # Nur eine Gruppe
php artisan cache:warm --dry-run # Zeigt was gewärmt würde

Rate-Limiting verhindert DB-Last-Spitze nach Deploy. Standard: 30 Keys/min via CACHE_WARM_RATE_PER_MINUTE. 0 deaktiviert Auto-Warming.

Hit-Rate-Tracking (E7)

Laravel feuert CacheHit/CacheMissed-Events bei jedem Cache-Zugriff. Der CacheHitRateTracker aggregiert pro Request in-memory + flusht am Request-Ende als Redis-Pipeline:

cache_stats:{group}:{date}:hits    (TTL 24h)
cache_stats:{group}:{date}:misses (TTL 24h)

Der SnapshotCacheMetrics Job (alle 15min) übernimmt diese Counter in die cache_monitoring_snapshots-Tabelle für Charts. Snapshots älter als 7 Tage werden täglich gepruned.

Web-Vitals Redis-Buffer (E13)

POST /api/web-vitals schreibt nicht mehr direkt in PostgreSQL, sondern pusht raw Messwerte in einen Redis-Buffer:

Browser → POST /api/web-vitals
→ Redis: RPUSH pb:web-vitals:buffer {json} (~0.1ms)
→ 200 {"ok": true}

Scheduled Job (alle 5 Min):
web-vitals:flush
→ LRANGE + LTRIM (atomar — keine Race-Condition mit neuen Einträgen)
→ Aggregation nach (date, url, metric_name, device_type)
→ 1 Bulk-Upsert pro Gruppe in PostgreSQL

Vorteil: Response-Time <1ms statt ~5ms vorher, DB-Queries skalieren nicht mehr mit Page-Views (nur mit unique URL/Metric-Kombinationen).

Datenverlust-Risiko: max. 5 Min bei Redis-Restart. Akzeptabel weil Web Vitals statistische Daten sind.

Fallstricke

  1. Redis-Neustart verliert Cache — automatisch via Cache::remember() rebuilt + forever-Keys liegen in PostgreSQL.
  2. FLUSHDB killt Scheduler-Locks — Admin-Seite warnt explizit. Empfohlen: nur Gruppen leeren, FLUSHDB nur als letztes Mittel.
  3. phpredis-Crash — Failover-Store fängt das ab (PostgreSQL-Cache als Fallback).
  4. RAM-Überlaufmaxmemory 256mb + allkeys-lru Eviction. Monitoring warnt ab 80%.
  5. SCAN-Performance — Bei <10K Keys vernachlässigbar (~10ms). KEYS-Befehl ist verboten (blockiert Redis).

Migration vs. Database-Cache

AspektDatabase-Cache (alt)Redis-Cache (neu)
Read-Latenz~2-5ms (SELECT)sub-ms
Write-Latenz~2-5ms (INSERT/UPDATE)sub-ms
Atomare Counter❌ (race-condition)✓ (INCR)
Persistenz❌ (RAM, flüchtig)
Bulk-ClearSQL LIKESCAN + DEL (non-blocking)
forever-Keys❌ → bleiben in DB
FailoverDB als Fallback

Rollback

Bei Problemen einfach .env zurück:

CACHE_STORE=database

Plus php artisan config:cache. Die cache + cache_locks Tabellen existieren weiterhin → kein Datenverlust.