Einleitung: Warum Caching in Rust eine Herausforderung und ein Vorteil ist
\nIn hochbelasteten Systemen mit Rust ist Caching nicht mehr nur das einfache Hinzufügen einer Bibliothek. Aufgrund des strengen Eigentümersystems, des Fehlens eines Garbage Collectors und der Anforderungen an die Threadsicherheit müssen traditionelle Ansätze überdacht werden. Rust bietet jedoch einzigartige Möglichkeiten: vorhersagbare Antwortzeiten, keine Gemeinkosten durch GC und vollständige Kontrolle über den Speicher. In diesem Artikel werden wir fortgeschrittene Caching-Techniken behandeln: von verteilten Lösungen (Redis, Memcached) bis hin zu integrierten Caches (moka, lru) und deren Kombination.
\n\n1. Asynchrone Arbeit mit Redis über deadpool-redis
\n1.1. Warum nicht direkt redis-rs?
\nDer Client redis-rs ist synchron. Für asynchrone Anwendungen mit tokio wird ein Verbindungspool benötigt. deadpool-redis bietet einen asynchronen Pool, der Verbindungen verwaltet und bei Ausfällen automatisch neu verbindet.
1.2. Beispiel: Caching mit TTL und Serialisierung
\nuse deadpool_redis::{Config, Runtime, Pool};\nuse serde::{Serialize, Deserialize};\nuse tokio::sync::OnceCell;\n\n#[derive(Serialize, Deserialize, Clone)]\nstruct User {\n id: u64,\n name: String,\n email: String,\n}\n\nasync fn get_pool() -> Pool {\n static POOL: OnceCell<Pool> = OnceCell::const_new();\n POOL.get_or_init(|| async {\n let cfg = Config::from_url("redis://127.0.0.1/");\n cfg.create_pool(Some(Runtime::Tokio1)).unwrap()\n }).await.clone()\n}\n\nasync fn get_user_cached(user_id: u64) -> Option<User> {\n let pool = get_pool().await;\n let mut conn = pool.get().await.unwrap();\n let key = format!("user:{}", user_id);\n\n // Versuch, aus dem Cache zu lesen\n if let Ok(Some(data)) = redis::cmd("GET").arg(&key).query_async::<Option<Vec<u8>>>(&mut conn).await {\n if let Ok(user) = bincode::deserialize::<User>(&data) {\n return Some(user);\n }\n }\n None\n}\n\nasync fn set_user_cache(user: &User) {\n let pool = get_pool().await;\n let mut conn = pool.get().await.unwrap();\n let key = format!("user:{}", user.id);\n let data = bincode::serialize(user).unwrap();\n\n // TTL 300 Sekunden\n let _: () = redis::cmd("SETEX")\n .arg(&key)\n .arg(300)\n .arg(&data)\n .query_async(&mut conn)\n .await\n .unwrap();\n}\n\n1.3. Fortgeschrittene Strategie: Cache-Aside mit Sperren
\nBei einem Cache Stampede (wenn viele Anfragen gleichzeitig versuchen, den Cache neu zu füllen) verwenden Sie einen Mutex auf Anwendungsebene:
\nuse tokio::sync::Mutex;\nuse std::collections::HashMap;\n\nstruct CacheLock {\n locks: Mutex<HashMap<String, tokio::sync::Semaphore>>,\n}\n\nimpl CacheLock {\n async fn get_or_compute<F, Fut, T>(&self, key: &str, f: F) -> T\n where\n F: FnOnce() -> Fut,\n Fut: Future<Output = T>,\n {\n let sem = {\n let mut map = self.locks.lock().await;\n map.entry(key.to_string())\n .or_insert_with(|| tokio::sync::Semaphore::new(1))\n .clone()\n };\n let _permit = sem.acquire().await.unwrap();\n f().await\n }\n}\n\n2. Memcached: Leichtigkeit und Geschwindigkeit mit memcache-async
\n2.1. Besonderheiten von Memcached für Rust
\nMemcached ist einfacher als Redis: keine Persistenz und komplexen Datenstrukturen, aber es ist schneller für einfache Key-Value-Operationen. Die Crate memcache-async bietet einen asynchronen Client für tokio.
2.2. Beispiel: Batch-Laden und Pipelining
\nuse memcache_async::{Client, Protocol, Value};\nuse bytes::Bytes;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let client = Client::connect_async("memcache://127.0.0.1:11211").await?;\n\n // P