Кэширование в Rust: Redis, Memcached и встроенные решения (продвинутый уровень)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему кэширование в Rust — это вызов и преимущество

В высоконагруженных системах на Rust кэширование перестаёт быть простым добавлением библиотеки. Из-за строгой системы владения, отсутствия сборщика мусора и требований к потокобезопасности, традиционные подходы требуют переосмысления. Однако Rust даёт уникальные возможности: предсказуемое время отклика, нулевые накладные расходы на GC и полный контроль над памятью. В этой статье мы разберём продвинутые техники кэширования: от распределённых решений (Redis, Memcached) до встроенных кэшей (moka, lru) и их комбинации.

1. Асинхронная работа с Redis через deadpool-redis

1.1. Почему не redis-rs напрямую?

Клиент redis-rs синхронный. Для асинхронных приложений на tokio требуется пул соединений. deadpool-redis предоставляет асинхронный пул, который управляет подключениями и автоматически переподключается при сбоях.

1.2. Пример: кэширование с TTL и сериализацией

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

    // Попытка прочитать из кэша
    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 секунд
    let _: () = redis::cmd("SETEX")
        .arg(&key)
        .arg(300)
        .arg(&data)
        .query_async(&mut conn)
        .await
        .unwrap();
}

1.3. Продвинутая стратегия: Cache-Aside с блокировками

При cache stampede (когда множество запросов одновременно пытаются перезаполнить кэш) используйте мьютекс на уровне приложения:

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: лёгкость и скорость с memcache-async

2.1. Особенности Memcached для Rust

Memcached проще Redis: нет персистентности и сложных структур данных, но он быстрее для простых key-value операций. Крейт memcache-async предоставляет асинхронный клиент на tokio.

2.2. Пример: пакетная загрузка и конвейеризация

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

    // Пакетная установка
    let items = vec![
        ("key1", b"value1" as &[u8]),
        ("key2", b"value2"),
        ("key3", b"value3"),
    ];
    for (key, val) in &items {
        client.set(key, val, 300).await?; // TTL 300 сек
    }

    // Конвейеризация через get_multi
    let keys = vec!["key1", "key2", "key3"];
    let results: Vec<Option<Value>> = client.get_multi(&keys).await?;
    for (key, val) in keys.iter().zip(results.iter()) {
        if let Some(v) = val {
            println!("{}: {:?}", key, String::from_utf8_lossy(&v.0));
        }
    }
    Ok(())
}

2.3. Когда выбирать Memcached вместо Redis?

  • Чистые key-value с TTL
  • Максимальная производительность чтения (меньше накладных расходов на парсинг протокола)
  • Отсутствие необходимости в pub/sub, списках или множествах

3. Встроенные решения: moka и lru — когда внешние сервисы не нужны

3.1. Moka: высокопроизводительный concurrent cache

Крейт moka (форк caffeine из Java) предоставляет потокобезопасный кэш с автоматической вытеснением по LFU/LRU. Идеален для in-process кэширования в веб-серверах.

use moka::sync::Cache;
use std::time::Duration;

fn create_cache() -> Cache<String, Vec<u8>> {
    Cache::builder()
        .max_capacity(10_000)           // Максимум 10 000 записей
        .time_to_live(Duration::from_secs(600)) // TTL 10 минут
        .time_to_idle(Duration::from_secs(120)) // Удалить, если не использовалось 2 минуты
        .build()
}

fn main() {
    let cache = create_cache();
    cache.insert("key".to_string(), b"expensive_data".to_vec());
    if let Some(data) = cache.get("key") {                

Блоги

Рекомендаций по книгам