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:
| Gruppe | Prefix(e) | Auto-Rebuild | TTL-Bereich |
|---|---|---|---|
explore | explore:, discover: | ✓ | 24h |
public_explorer | public-explorer: | ✓ | 1h–24h |
recommendations | recommended_profiles, trending_slider: | ✓ | 1h–6h |
pipeline | pipeline:, cron:heartbeat:, health:, deploy: | ⚠ Cron | 5min–48h |
youtube | yt_, youtube_ | ✓ | 5min–24h |
instagram | instagram_ | ✓ | 1h |
tags | blocked_tags:, tag_aliases: | ✓ | 1h |
admin | admin: | ✓ | 30s–1h |
server | server:, db_monitoring:, pulse:, error-monitor: | ✓ | 30s–24h |
mail | mail:flood: | ⚠ Counter-Reset | 60s–24h |
video_trends | video-trends: | ✓ | 1h–24h |
collector | collector: | ✓ | 5min |
explore_sections | explore_sections: | ✓ | 1h |
r2_storage | r2: | ✓ (nur bei s3/dual-write) | 60s–7d |
rustfs_storage | storage_monitoring: | ✓ (nur bei rustfs/dual-write) | 15min |
turnstile | turnstile: | ✓ | 5min |
cache_stats | cache_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
ClearCacheGroupJob + 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 BereichWARNING—evicted_keys > 0ODERmemory > 80%ODERfragmentation > 1.5FAILED— 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
- Redis-Neustart verliert Cache — automatisch via
Cache::remember()rebuilt +forever-Keys liegen in PostgreSQL. - FLUSHDB killt Scheduler-Locks — Admin-Seite warnt explizit. Empfohlen: nur Gruppen leeren, FLUSHDB nur als letztes Mittel.
- phpredis-Crash — Failover-Store fängt das ab (PostgreSQL-Cache als Fallback).
- RAM-Überlauf —
maxmemory 256mb+allkeys-lruEviction. Monitoring warnt ab 80%. - SCAN-Performance — Bei
<10KKeys vernachlässigbar (~10ms). KEYS-Befehl ist verboten (blockiert Redis).
Migration vs. Database-Cache
| Aspekt | Database-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-Clear | SQL LIKE | SCAN + DEL (non-blocking) |
forever-Keys | ✓ | ❌ → bleiben in DB |
| Failover | — | DB 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.