Caching
Caching speichert das Ergebnis teurer Arbeit — eine schwere Query, ein
gerendertes Fragment, einen Drittanbieter-API-Aufruf — sodass der nächste
Request es sofort erhält, statt es neu zu berechnen. Rustango gibt dir einen
Cache-Trait mit austauschbaren Backends (In-Memory, Redis, Datenbank), einen
Compute-on-Miss-Helfer (get_or_set) und typisierte JSON-Helfer. Tausche das
Backend, ohne eine einzige Aufrufstelle anzufassen — wie Djangos Cache-Framework
oder Laravels Cache-Fassade.
Ein Begriff hier neu für dich? Cache, TTL, Key, Backend — siehe das Glossar.
Quelle:
rustango::cache(Cache,InMemoryCache,NullCache,get_or_set,get_json,set_json,BoxedCache,from_settings) — hinter dercache-Feature (standardmäßig aktiv).RedisCachebenötigt diecache-redis-Feature (standardmäßig aus).Ausführbare Version: jeder Codeausschnitt ist aus
cache_doc.rskopiert (cargo test -p rustango --test cache_doc); das Datenbank-Backend wird per Dogfooding auf SQLite durchcache_db_backend_sqlite_live.rserprobt.
Inhaltsverzeichnis
- Schritt 1 — Ein Backend wählen
- Schritt 2 — get / set / delete
- Schritt 3 — get_or_set (Cache-Aside)
- Typisierte JSON-Werte
- TTL und Ablauf
- Backends tauschen
- Referenz
- Siehe auch
Schritt 1 — Ein Backend wählen
Jedes Backend implementiert denselben Cache-Trait, dein Code ist also
identisch, welches du auch wählst. Der App-Code hält einen BoxedCache
(Arc<dyn Cache>) und nennt nie den konkreten Typ:
use rustango::cache::{BoxedCache, InMemoryCache};
use std::sync::Arc;
let cache: BoxedCache = Arc::new(InMemoryCache::new());
| Backend | Feature | Verwenden für |
|---|---|---|
InMemoryCache | cache | Dev, Tests, Einzelprozess (HashMap pro Prozess + TTL) |
RedisCache | cache-redis | Produktion; über Replicas geteilt |
DbCache | cache | Produktion ohne Redis; eine rustango_cache-Tabelle |
NullCache | cache | Caching deaktivieren (jeder Read verfehlt) — praktisch in Tests |
Schritt 2 — get / set / delete
Der Kern des Traits sind vier async-Methoden. set nimmt ein optionales TTL
(None = kein Ablauf); get gibt Option<String> zurück (None bei einem
Miss):
use rustango::cache::{Cache, InMemoryCache};
let cache = InMemoryCache::new();
assert_eq!(cache.get("greeting").await?, None); // miss
cache.set("greeting", "hello", None).await?; // store, no expiry
assert_eq!(cache.get("greeting").await?.as_deref(), Some("hello"));
assert!(cache.exists("greeting").await?);
cache.delete("greeting").await?; // gone
Es gibt auch Batch-Varianten — get_many / set_many / delete_many.
Schritt 3 — get_or_set (Cache-Aside)
Das ist die, zu der du am häufigsten greifst. get_or_set gibt den gecachten
Wert zurück, oder — bei einem Miss — führt deine Factory aus, speichert das
Ergebnis mit einem TTL und gibt es zurück. Die Factory läuft nur bei einem
Miss:
use rustango::cache::get_or_set;
use std::time::Duration;
let stats: HomeStats = get_or_set(
&*cache, // &dyn Cache
"home:stats",
|| async { compute_home_stats(&pool).await }, // runs only on a miss
Some(Duration::from_secs(60)), // cache for 60s
).await?;
Der zugrunde liegende Test ruft get_or_set zweimal für denselben Key auf und
stellt sicher, dass die Factory genau einmal lief — der zweite Aufruf wird
aus dem Cache bedient.
Bei Schreibvorgängen invalidieren. Cache-Aside bedeutet veraltete Daten, bis das TTL abläuft. Für Daten, die sich ändern, mache auch
delete(key), wenn du sie schreibst — z. B. aus einempost_save-Signal — damit der nächste Read neu berechnet.
Typisierte JSON-Werte
get_json / set_json serialisieren jeden Serialize/Deserialize-Typ nach
JSON, sodass du Structs und Listen cachst, nicht nur Strings:
use rustango::cache::{get_json, set_json};
#[derive(serde::Serialize, serde::Deserialize)]
struct Profile { id: i64, name: String }
set_json(&*cache, "profile:7", &profile, None).await?;
let back: Option<Profile> = get_json(&*cache, "profile:7").await?; // None on a miss
(get_or_set verwendet diese unter der Haube, weshalb sein Werttyp
Serialize + Deserialize sein muss.)
TTL und Ablauf
Übergib eine Duration an set (oder get_or_set) und der Eintrag verschwindet
danach. Verifiziert: ein 50-ms-Eintrag ist sofort lesbar und nach 80 ms weg.
cache.set("flash", "x", Some(Duration::from_millis(50))).await?;
// ...50ms later...
assert_eq!(cache.get("flash").await?, None); // expired
InMemoryCache::with_default_ttl(d) setzt ein Standard-TTL, das angewendet wird,
wenn du None übergibst.
Backends tauschen
Weil alles der Cache-Trait ist, ist der Wechsel von In-Memory zu Redis eine
einzeilige Änderung beim Start — meist von der Konfiguration gesteuert, sodass sie
sich pro Umgebung unterscheidet:
// Build the cache from `[cache]` settings (backend = "memory" | "redis" | "db" | "null").
let cache: BoxedCache = rustango::cache::from_settings(&settings.cache);
In der Produktion richtest du es auf Redis (über alle deine Replicas geteilt):
use rustango::cache::RedisCache; // needs the `cache-redis` feature
let cache: BoxedCache = std::sync::Arc::new(RedisCache::new("redis://localhost").await?);
Dieselben get / set / get_or_set-Aufrufe — nur der Konstruktor hat sich
geändert.
Referenz
Cache-Trait: get · set(key, value, ttl) · delete · exists ·
get_many / set_many / delete_many · get_or(key, default).
Freie Helfer: get_or_set(cache, key, factory, ttl) ·
get_json / set_json · from_settings(&CacheSettings).
Was auf dem Cache aufbaut: serverseitige Sessions,
verteiltes Rate Limiting (CacheRateLimitLayer),
Idempotenz-Schlüssel und Feature-Flags nehmen alle einen BoxedCache — sodass
eine einzige Redis-Instanz sie alle stützt.
Siehe auch
- Hintergrund-Jobs — die andere Hälfte, um Requests schnell zu halten (Arbeit aufschieben, statt ihr Ergebnis zu cachen).
- Sessions — ein serverseitiger Store, der auf
Cacheaufbaut. - Middleware —
CacheRateLimitLayerteilt einen Zähler über Replicas hinweg via Cache. - ORM-Cookbook — gecachte Reads aus einem
post_save-Signal invalidieren.
