Introduction : Pourquoi la mise en cache en Rust est un défi et un avantage
\nDans les systèmes à forte charge en Rust, la mise en cache cesse d'être un simple ajout de bibliothèque. En raison du système de propriété strict, de l'absence de ramasse-miettes et des exigences de sécurité des threads, les approches traditionnelles nécessitent une refonte. Cependant, Rust offre des possibilités uniques : temps de réponse prévisible, frais généraux nuls pour le GC et contrôle total de la mémoire. Dans cet article, nous aborderons les techniques avancées de mise en cache : des solutions distribuées (Redis, Memcached) aux caches intégrés (moka, lru) et leurs combinaisons.
\n\n1. Travail asynchrone avec Redis via deadpool-redis
\n1.1. Pourquoi pas redis-rs directement ?
\nLe client redis-rs est synchrone. Pour les applications asynchrones sur tokio, un pool de connexions est nécessaire. deadpool-redis fournit un pool asynchrone qui gère les connexions et se reconnecte automatiquement en cas de panne.
1.2. Exemple : mise en cache avec TTL et sérialisation
\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 // Tentative de lecture depuis le cache\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 secondes\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. Stratégie avancée : Cache-Aside avec verrous
\nEn cas de cache stampede (lorsque plusieurs requêtes tentent simultanément de repeupler le cache), utilisez un mutex au niveau de l'application :
\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 : légèreté et rapidité avec memcache-async
\n2.1. Particularités de Memcached pour Rust
\nMemcached est plus simple que Redis : pas de persistance ni de structures de données complexes, mais il est plus rapide pour les opérations simples de type key-value. La crate memcache-async fournit un client asynchrone sur tokio.
2.2. Exemple : chargement par lots et pipeline
\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