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
- Documentação oficial da crate redis — Documentação completa com todos os módulos, traits e exemplos de uso
- Redis Pub/Sub Documentation — Documentação oficial do Redis sobre o modelo publisher/subscriber
- r2d2_redis crate — Pool de conexões Redis para aplicações concorrentes em Rust
- Tutorial: Redis com Rust e Axum — Exemplo prático de aplicação web com cache Redis e Axum
- fred crate (alternativa async) — Cliente Redis assíncrono nativo para Rust com suporte a cluster e sentinel
- Redis Commands Reference — Referência completa de comandos Redis com exemplos e documentação detalhada