Redis com redis crate: cache e pub/sub

1. Introdução ao Redis e à crate redis

Redis é um armazenamento de estrutura de dados em memória, open-source, que funciona como banco chave-valor. Suporta diversos tipos de dados como strings, hashes, listas, sets e sorted sets, além de oferecer funcionalidades avançadas como cache com expiração, filas de mensagens e pub/sub. Por sua velocidade e simplicidade, é amplamente utilizado em aplicações Rust para cache, sessões de usuário, filas de tarefas e comunicação em tempo real.

A crate redis é a biblioteca cliente oficial para Rust, oferecendo uma API completa para interagir com servidores Redis. Suporta conexões síncronas e assíncronas, pipelining, transações, pub/sub e diversos tipos de dados Redis. A crate é madura, bem documentada e compatível com os principais runtimes assíncronos como Tokio e async-std.

Para configurar o projeto, adicione as dependências no Cargo.toml:

[dependencies]
redis = { version = "0.25", features = ["tokio-comp"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

A string de conexão padrão é redis://127.0.0.1:6379. Para conexões com senha: redis://:password@127.0.0.1:6379.

2. Conexão e operações básicas

Estabelecer uma conexão com Redis é simples. A crate oferece Client::open para criar um cliente e get_connection para conexões síncronas:

use redis::{Client, Commands, RedisResult};

fn main() -> RedisResult<()> {
    let client = Client::open("redis://127.0.0.1:6379")?;
    let mut con = client.get_connection()?;

    // Operações básicas
    con.set("chave", "valor")?;
    let valor: String = con.get("chave")?;
    println!("Valor: {}", valor);

    con.del("chave")?;
    let existe: bool = con.exists("chave")?;
    println!("Existe: {}", existe);

    Ok(())
}

A trait Commands fornece métodos para todos os comandos Redis. O tratamento de erros usa RedisError e RedisResult<T>, que é um alias para Result<T, RedisError>. O operador ? propaga erros de forma elegante.

Para conexões assíncronas com Tokio:

use redis::AsyncCommands;

#[tokio::main]
async fn main() -> redis::RedisResult<()> {
    let client = redis::Client::open("redis://127.0.0.1:6379")?;
    let mut con = client.get_async_connection().await?;

    con.set("async_key", "async_val").await?;
    let val: String = con.get("async_key").await?;
    println!("Async: {}", val);

    Ok(())
}

3. Cache com Redis: implementação prática

Uma das aplicações mais comuns do Redis é como cache. A estratégia cache-aside (lazy loading) é implementada verificando o cache primeiro, e em caso de miss, buscando no banco de dados e populando o cache.

use redis::Commands;
use serde::{Deserialize, Serialize};
use std::time::Duration;

#[derive(Serialize, Deserialize, Debug)]
struct Usuario {
    id: u32,
    nome: String,
    email: String,
}

fn buscar_usuario(con: &mut redis::Connection, id: u32) -> redis::RedisResult<Usuario> {
    let chave = format!("usuario:{}", id);

    // Verificar cache primeiro
    if let Ok(dados) = con.get::<_, String>(&chave) {
        if let Ok(usuario) = serde_json::from_str(&dados) {
            println!("Cache hit para usuário {}", id);
            return Ok(usuario);
        }
    }

    // Cache miss: buscar do "banco" (simulado aqui)
    println!("Cache miss para usuário {}", id);
    let usuario = Usuario {
        id,
        nome: format!("Usuário {}", id),
        email: format!("user{}@exemplo.com", id),
    };

    // Popular cache com TTL de 60 segundos
    let dados_json = serde_json::to_string(&usuario).unwrap();
    let _: () = con.set_ex(&chave, dados_json, 60)?;

    Ok(usuario)
}

O comando SET_EX (ou set_ex) define um valor com tempo de expiração em segundos. A serialização com serde_json permite armazenar structs complexas como strings JSON.

4. Cache com Redis: otimizações e boas práticas

Para aplicações concorrentes, usar um pool de conexões é essencial. A crate r2d2_redis integra o Redis com o pool de conexões r2d2:

use r2d2_redis::RedisConnectionManager;

fn criar_pool() -> r2d2::Pool<RedisConnectionManager> {
    let manager = RedisConnectionManager::new("redis://127.0.0.1:6379").unwrap();
    r2d2::Pool::builder()
        .max_size(10)
        .build(manager)
        .unwrap()
}

Operações em lote com MGET e MSET reduzem drasticamente a latência:

fn buscar_multiplos(con: &mut redis::Connection, ids: &[u32]) -> redis::RedisResult<Vec<Option<String>>> {
    let chaves: Vec<String> = ids.iter().map(|id| format!("usuario:{}", id)).collect();
    let resultados: Vec<Option<String>> = con.mget(&chaves)?;
    Ok(resultados)
}

Para invalidação seletiva, use DEL com padrões de chave:

fn invalidar_cache_usuario(con: &mut redis::Connection, id: u32) -> redis::RedisResult<()> {
    let chave = format!("usuario:{}:profile", id);
    con.del(&chave)?;
    Ok(())
}

5. Pub/Sub com Redis: conceitos e configuração

O modelo pub/sub do Redis permite comunicação assíncrona entre processos. Publishers enviam mensagens para canais, e subscribers recebem essas mensagens em tempo real.

Para configurar um subscriber:

use redis::{Client, PubSubCommands};

fn subscriber_exemplo() -> redis::RedisResult<()> {
    let client = Client::open("redis://127.0.0.1:6379")?;
    let mut con = client.get_connection()?;
    let mut pubsub = con.as_pubsub();

    pubsub.subscribe("canal_notificacoes")?;

    loop {
        let msg = pubsub.get_message()?;
        let payload: String = msg.get_payload()?;
        let canal: String = msg.get_channel_name()?;
        println!("Recebido em '{}': {}", canal, payload);
    }
}

Para o publisher:

fn publisher_exemplo(con: &mut redis::Connection) -> redis::RedisResult<()> {
    con.publish("canal_notificacoes", "Mensagem de teste")?;
    Ok(())
}

6. Pub/Sub com Redis: exemplos práticos em Rust

Um subscriber assíncrono mais robusto:

use redis::aio::Connection;
use redis::AsyncCommands;

async fn subscriber_async(client: &redis::Client) -> redis::RedisResult<()> {
    let mut con = client.get_async_connection().await?;
    let mut pubsub = con.into_pubsub();

    pubsub.subscribe("canal1").await?;
    pubsub.subscribe("canal2").await?;

    loop {
        let msg = pubsub.on_message().await?;
        let payload: String = msg.get_payload()?;
        let canal: String = msg.get_channel_name()?;

        match canal.as_str() {
            "canal1" => println!("Canal 1: {}", payload),
            "canal2" => println!("Canal 2: {}", payload),
            _ => println!("Outro: {}", payload),
        }
    }
}

Publisher síncrono para disparar eventos de cache:

fn notificar_atualizacao_cache(con: &mut redis::Connection, usuario_id: u32) -> redis::RedisResult<()> {
    let mensagem = format!("{{\"tipo\":\"atualizacao\",\"usuario_id\":{}}}", usuario_id);
    con.publish("cache_updates", mensagem)?;
    Ok(())
}

7. Integração com async runtime (Tokio)

Usando redis::aio::MultiplexedConnection para conexões assíncronas multiplexadas:

use redis::aio::MultiplexedConnection;
use redis::AsyncCommands;

async fn exemplo_multiplexado(client: &redis::Client) -> redis::RedisResult<()> {
    let mut con: MultiplexedConnection = client.get_multiplexed_async_connection().await?;

    // Várias operações concorrentes
    let (val1, val2) = tokio::join!(
        con.get::<_, String>("chave1"),
        con.get::<_, String>("chave2")
    );

    println!("{} {}", val1?, val2?);
    Ok(())
}

Exemplo com servidor Axum integrando cache e pub/sub:

use axum::{Router, routing::get, extract::State};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    redis_client: redis::Client,
}

async fn health_check(State(state): State<AppState>) -> String {
    let mut con = state.redis_client.get_async_connection().await.unwrap();
    let _: () = con.set("health", "ok").await.unwrap();
    "OK".to_string()
}

#[tokio::main]
async fn main() {
    let client = redis::Client::open("redis://127.0.0.1:6379").unwrap();
    let state = AppState { redis_client: client };

    // Subscriber em background
    let client_clone = state.redis_client.clone();
    tokio::spawn(async move {
        subscriber_async(&client_clone).await.unwrap();
    });

    let app = Router::new()
        .route("/health", get(health_check))
        .with_state(state);

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

8. Considerações finais e próximos passos

A crate redis oferece uma base sólida para trabalhar com Redis em Rust, mas existem alternativas como fred (totalmente assíncrona) e mobc-redis (pool de conexões). Para ambientes de cluster Redis, a crate redis-cluster é necessária, pois redis não suporta clustering nativamente.

Limitações importantes: a crate redis não gerencia reconexões automaticamente em modo síncrono, e o suporte a respostas de pub/sub em modo assíncrono requer atenção ao gerenciamento de tasks.

Para aprofundar, explore a documentação oficial da crate, exemplos no GitHub e integrações com SQLx ou Diesel para persistência combinada com cache Redis. A combinação de Redis para cache e pub/sub com Rust oferece performance excepcional para aplicações modernas.

Referências