Introdução: Por que o cache em Rust é um desafio e uma vantagem
Em sistemas de alta carga em Rust, o cache deixa de ser uma simples adição de biblioteca. Devido ao sistema estrito de ownership, à ausência de garbage collector e aos requisitos de thread safety, as abordagens tradicionais exigem repensar. No entanto, Rust oferece capacidades únicas: tempo de resposta previsível, custos indiretos zero com GC e controle total sobre a memória. Neste artigo, abordaremos técnicas avançadas de cache: desde soluções distribuídas (Redis, Memcached) até caches embutidos (moka, lru) e suas combinações.
1. Trabalho assíncrono com Redis via deadpool-redis
1.1. Por que não usar redis-rs diretamente?
O cliente redis-rs é síncrono. Para aplicações assíncronas em tokio, é necessário um pool de conexões. O deadpool-redis fornece um pool assíncrono que gerencia as conexões e reconecta automaticamente em caso de falhas.
1.2. Exemplo: cache com TTL e serialização
use deadpool_redis::{Config, Runtime, Pool};use serde::{Serialize, Deserialize};use tokio::sync::OnceCell;
#[derive(Serialize, Deserialize, Clone)]struct User { id: u64, name: String, email: String,}
async fn get_pool() -> Pool { static POOL: OnceCell<Pool> = OnceCell::const_new(); POOL.get_or_init(|| async { let cfg = Config::from_url("redis://127.0.0.1/"); cfg.create_pool(Some(Runtime::Tokio1)).unwrap() }).await.clone()}
async fn get_user_cached(user_id: u64) -> Option<User> { let pool = get_pool().await; let mut conn = pool.get().await.unwrap(); let key = format!("user:{}", user_id);
// Tentativa de ler do cache if let Ok(Some(data)) = redis::cmd("GET").arg(&key).query_async::<Option<Vec<u8>>>(&mut conn).await { if let Ok(user) = bincode::deserialize::<User>(&data) { return Some(user); } } None}
async fn set_user_cache(user: &User) { let pool = get_pool().await; let mut conn = pool.get().await.unwrap(); let key = format!("user:{}", user.id); let data = bincode::serialize(user).unwrap();
// TTL 300 segundos let _: () = redis::cmd("SETEX") .arg(&key) .arg(300) .arg(&data) .query_async(&mut conn) .await .unwrap();}1.3. Estratégia avançada: Cache-Aside com locks
Em caso de cache stampede (quando múltiplas requisições tentam recarregar o cache simultaneamente), use um mutex no nível da aplicação:
use tokio::sync::Mutex;use std::collections::HashMap;
struct CacheLock { locks: Mutex<HashMap<String, tokio::sync::Semaphore>>,}
impl CacheLock { async fn get_or_compute<F, Fut, T>(&self, key: &str, f: F) -> T where F: FnOnce() -> Fut, Fut: Future<Output = T>, { let sem = { let mut map = self.locks.lock().await; map.entry(key.to_string()) .or_insert_with(|| tokio::sync::Semaphore::new(1)) .clone() }; let _permit = sem.acquire().await.unwrap(); f().await }}2. Memcached: leveza e velocidade com memcache-async
2.1. Características do Memcached para Rust
O Memcached é mais simples que o Redis: não possui persistência nem estruturas de dados complexas, mas é mais rápido para operações simples de key-value. A crate memcache-async fornece um cliente assíncrono baseado em tokio.
2.2. Exemplo: carregamento em lote e pipeline
use memcache_async::{Client, Protocol, Value};use bytes::Bytes;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::connect_async("memcache://127.0.0.1:11211").await?;
// P