Caching in Rust: Redis, Memcached, and Built-in Solutions (Advanced Level)

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Caching in Rust is a Challenge and an Advantage

In high-load systems built with Rust, caching ceases to be a simple matter of adding a library. Due to the strict ownership system, the absence of a garbage collector, and thread safety requirements, traditional approaches require rethinking. However, Rust offers unique capabilities: predictable response times, zero GC overhead, and full control over memory. In this article, we will explore advanced caching techniques: from distributed solutions (Redis, Memcached) to built-in caches (moka, lru) and their combinations.



1. Asynchronous Work with Redis via deadpool-redis

1.1. Why Not redis-rs Directly?

The redis-rs client is synchronous. For asynchronous applications on tokio, a connection pool is required. deadpool-redis provides an asynchronous pool that manages connections and automatically reconnects on failures.



1.2. Example: Caching with TTL and Serialization

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);

// Attempt to read from 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 seconds let _: () = redis::cmd("SETEX") .arg(&key) .arg(300) .arg(&data) .query_async(&mut conn) .await .unwrap();}


1.3. Advanced Strategy: Cache-Aside with Locks

For cache stampede (when many requests simultaneously try to repopulate the cache), use an application-level mutex:

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: Lightness and Speed with memcache-async

2.1. Memcached Features for Rust

Memcached is simpler than Redis: no persistence and complex data structures, but it is faster for simple key-value operations. The memcache-async crate provides an asynchronous client on tokio.



2.2. Example: Batch Loading and Pipelining

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

Blogs

Book Recommendations